First Commit
This commit is contained in:
43
frontend/src/App.js
Normal file
43
frontend/src/App.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom'
|
||||
import Header from './components/Header';
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import NewPurchase from './pages/NewPurchase';
|
||||
import NewProduct from './pages/NewProduct';
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
import Purchase from './pages/Purchase';
|
||||
import {ToastContainer} from 'react-toastify'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Router>
|
||||
<div className='container'>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<PrivateRoute />}>
|
||||
<Route path="/" element={<Home />} />
|
||||
</Route>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/new-purchase" element={<PrivateRoute />}>
|
||||
<Route path="/new-purchase" element={<NewPurchase />} />
|
||||
</Route>
|
||||
<Route path="/new-product" element={<PrivateRoute />}>
|
||||
<Route path="/new-product" element={<NewProduct />} />
|
||||
</Route>
|
||||
<Route path="/purchase/:id" element={<PrivateRoute />}>
|
||||
<Route path="/purchase/:id" element={<Purchase />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
20
frontend/src/app/store.js
Normal file
20
frontend/src/app/store.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import authReducer from '../features/auth/authSlice';
|
||||
import productReducer from '../features/products/productSlice';
|
||||
import purchaseReducer from '../features/purchases/purchaseSlice';
|
||||
import currencyReducer from '../features/currencies/currencySlice';
|
||||
import languageReducer from '../features/languages/languageSlice';
|
||||
import groupReducer from '../features/groups/groupSlice';
|
||||
import subGroupReducer from '../features/subgroups/subGroupSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
product: productReducer,
|
||||
purchase: purchaseReducer,
|
||||
currency: currencyReducer,
|
||||
language: languageReducer,
|
||||
group: groupReducer,
|
||||
subgroup: subGroupReducer
|
||||
},
|
||||
});
|
||||
16
frontend/src/components/BackButton.jsx
Normal file
16
frontend/src/components/BackButton.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import {FaArrowCircleLeft} from 'react-icons/fa'
|
||||
import {Link} from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BackButton = ({url}) => {
|
||||
const { t } = useTranslation(["Purchase"]);
|
||||
|
||||
return (
|
||||
<Link className="btn btn-reverse btn-black" to={url}>
|
||||
<FaArrowCircleLeft /> {t("back")}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackButton
|
||||
61
frontend/src/components/Header.jsx
Normal file
61
frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import {FaSignInAlt, FaSignOutAlt, FaUser} from 'react-icons/fa'
|
||||
import {Link, useNavigate} from 'react-router-dom'
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {logout, reset} from '../features/auth/authSlice'
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function Header() {
|
||||
const {user} = useSelector(state => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation(["Header"]);
|
||||
|
||||
const onLogout = async() => {
|
||||
await dispatch(logout())
|
||||
dispatch(reset())
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="logo">
|
||||
<Link to='/'>GiecChallenge</Link>
|
||||
</div>
|
||||
<ul>
|
||||
{
|
||||
!user ? (
|
||||
<>
|
||||
<li>
|
||||
<Link to='/login'>
|
||||
<FaSignInAlt /> {t("login")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to='/register'>
|
||||
<FaUser /> {t("register")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<LanguageSelector />
|
||||
</li>
|
||||
</>) : (
|
||||
<>
|
||||
<li>
|
||||
<a onClick={onLogout} href='/'>
|
||||
<FaSignOutAlt /> {t("logout")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<LanguageSelector />
|
||||
</li>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
77
frontend/src/components/Home/HomePurchase.jsx
Normal file
77
frontend/src/components/Home/HomePurchase.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import ProgressBar from 'react-bootstrap/ProgressBar';
|
||||
import { reset, getpurchasesbydate, deletepurchase, getCO2bydate} from '../../features/purchases/purchaseSlice'
|
||||
import {LinePurchase} from './LinePurchase'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
export const HomePurchase = () => {
|
||||
const { isLoading, purchases, CO2Emissions } = useSelector((state) => state.purchase)
|
||||
const [startDate, setStartDate] = useState(new Date());
|
||||
const [endDate, setEndDate] = useState(new Date());
|
||||
const [purchasesUser, setPurchasesUser] = useState([]);
|
||||
const [CO2EmissionsPurcentage, setCO2EmissionsPurcentage] = useState(0.0);
|
||||
const { t } = useTranslation(["Home"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
setPurchasesUser(purchases);
|
||||
setCO2EmissionsPurcentage(CO2Emissions / (2500 / 365 * (endDate.getDate() - startDate.getDate() + 1)) * 100);
|
||||
}, [purchases, CO2Emissions])
|
||||
|
||||
useEffect(() => {
|
||||
changeDate();
|
||||
}, [startDate, endDate])
|
||||
|
||||
function changeDate() {
|
||||
dispatch(reset())
|
||||
dispatch(getpurchasesbydate({
|
||||
startDate: startDate.toLocaleDateString("es-CL"),
|
||||
endDate: endDate.toLocaleDateString("es-CL")
|
||||
}));
|
||||
dispatch(getCO2bydate({
|
||||
startDate: startDate.toLocaleDateString("es-CL"),
|
||||
endDate: endDate.toLocaleDateString("es-CL")
|
||||
}));
|
||||
}
|
||||
|
||||
const deletePurchase = (p) => {
|
||||
dispatch(reset());
|
||||
dispatch(deletepurchase(p)).then(
|
||||
changeDate()
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<div className='mbottom-10'>
|
||||
<ProgressBar now={CO2EmissionsPurcentage} label={`${CO2EmissionsPurcentage}%`} />
|
||||
</div>
|
||||
<div style={{display: "flex"}} className="form-group">
|
||||
<DatePicker dateFormat="dd/MM/yyyy" selected={startDate} className="form-control" onChange={(date) => {setStartDate(date);}} />
|
||||
<DatePicker dateFormat="dd/MM/yyyy" selected={endDate} className="form-control" onChange={(date) => {setEndDate(date);}} />
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='form-group width-20'>{t("date")}</div>
|
||||
<div className='form-group width-20'>{t("CO2Cost")}</div>
|
||||
<div className='form-group width-20'>{t("WaterCost")}</div>
|
||||
</div>
|
||||
{
|
||||
purchasesUser.map((purchaseUser, index) => {
|
||||
return (
|
||||
<LinePurchase key={index} purchaseUser={purchaseUser} onChange={(p) => deletePurchase(p)} />
|
||||
)
|
||||
})
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePurchase
|
||||
22
frontend/src/components/Home/LinePurchase.jsx
Normal file
22
frontend/src/components/Home/LinePurchase.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router-dom'
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
|
||||
export const LinePurchase = ({ purchaseUser, onChange }) => {
|
||||
return (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<div className='form-group width-20 inline-flex'>
|
||||
<Link to={`/purchase/${purchaseUser.id}`}>{new Date(Date.parse(purchaseUser.datePurchase)).toLocaleString("fr-FR", {month: '2-digit',day: '2-digit',year: 'numeric'})}</Link>
|
||||
</div>
|
||||
<div className='form-group width-20 inline-flex'>{parseFloat(purchaseUser.cO2Cost).toFixed(2)}</div>
|
||||
<div className='form-group width-20 inline-flex'>{parseFloat(purchaseUser.waterCost).toFixed(2)}</div>
|
||||
<div>
|
||||
<a onClick={() => onChange(purchaseUser.id)} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaTrash />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
22
frontend/src/components/LanguageSelector.jsx
Normal file
22
frontend/src/components/LanguageSelector.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const changeLanguage = (event) => {
|
||||
i18n.changeLanguage(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div onChange={changeLanguage}>
|
||||
<select name="language">
|
||||
<option value="FR">Français</option>
|
||||
<option value="EN">English</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
||||
30
frontend/src/components/NewProduct/NewGroup/LineGroup.jsx
Normal file
30
frontend/src/components/NewProduct/NewGroup/LineGroup.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LineGroup = ({ nameSelect, languageOptions, onChange }) => {
|
||||
const [group, setGroup] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
group,
|
||||
language,
|
||||
nameSelect
|
||||
});
|
||||
}, [onChange, group, language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="command">{t("group")}</label>
|
||||
<input type="language" name="group" id="group" className='form-control width-100' value={group} placeholder={t("group")} onChange={(e) => setGroup(e.target.value)} />
|
||||
<label htmlFor="language">{t("language")}</label>
|
||||
<select name="language" id="language" value={language} className="form-control width-100" onChange={(e) => setLanguage(e.target.value)}>
|
||||
{languageOptions}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
92
frontend/src/components/NewProduct/NewGroup/NewGroup.jsx
Normal file
92
frontend/src/components/NewProduct/NewGroup/NewGroup.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {reset, create} from '../../../features/groups/groupSlice'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { LineGroup } from './LineGroup'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NewGroup = ({ languagesOption }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.group)
|
||||
const [groups, setGroups] = useState([{key: 0, name: "group0" }])
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
const [lineGroupsData, setLineGroupsData] = useState([])
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
setGroups([{key: 0, name: "group0" }])
|
||||
setLineGroupsData([])
|
||||
toast.success(t("groupSuccess"))
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const newGroup = {
|
||||
names: lineGroupsData.map((groupLine) => (
|
||||
{
|
||||
name: groupLine.group,
|
||||
language: groupLine.language
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(newGroup))
|
||||
}
|
||||
|
||||
const addOrModifyLineLanguage = (lineLanguage) => {
|
||||
if (languagesOption[0] !== undefined && lineLanguage.language === '')
|
||||
lineLanguage.language = languagesOption[0].key;
|
||||
let existingLine = lineGroupsData.findIndex(lpd => lpd.nameSelect === lineLanguage.nameSelect);
|
||||
if (existingLine === -1)
|
||||
setLineGroupsData(lineGroupsData.concat(lineLanguage))
|
||||
else {
|
||||
lineGroupsData[existingLine] = lineLanguage
|
||||
setLineGroupsData(lineGroupsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewGroup = () => {
|
||||
setGroups(groups.concat([
|
||||
{key: groups.length, name: "group" + groups.length }
|
||||
]));
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("newGroup")}</legend>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
{
|
||||
groups.map((item) => (
|
||||
<LineGroup key={item.key} languageOptions={languagesOption} nameSelect={item.key} onChange={(e) => addOrModifyLineLanguage(e)}/>
|
||||
))
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewGroup()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewTranslation")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewGroup
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LineProductItem = ({ nameSelected, languageOptions, onChange }) => {
|
||||
const [productName, setProductName] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
nameSelected,
|
||||
productName,
|
||||
language
|
||||
});
|
||||
}, [onChange, productName, language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="product">{t("product")}</label>
|
||||
<input type="text" name="productName" id="productName" className='form-control width-100' value={productName} placeholder={t("product")} onChange={(e) => setProductName(e.target.value)} />
|
||||
<label htmlFor="language">{t("language")}</label>
|
||||
<select name="language" id="language" value={language} className="form-control width-100" onChange={(e) => setLanguage(e.target.value)}>
|
||||
{languageOptions}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {reset, create} from '../../../features/products/productSlice'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { LineProductItem } from './LineProductItem'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {SubGroupSearchBox} from '../../SearchBox/SubGroupSearchBox'
|
||||
|
||||
export const NewProductItem = ({ languagesOption }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.product)
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const [lineProductsData, setLineProductsData] = useState([{ nameSelected: "product0",productName: "", language: "" }])
|
||||
const [CO2Emissions, setCO2Emission] = useState('0')
|
||||
const [waterEmissions, setWaterEmission] = useState('0')
|
||||
const [amortization, setAmortization] = useState('0')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
setLineProductsData([{nameSelected: "product0", productName: "", language: "" }])
|
||||
toast.success(t("productSuccess"))
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const newProduct = {
|
||||
group : selectedValue,
|
||||
CO2: CO2Emissions,
|
||||
water: waterEmissions,
|
||||
amortization: amortization,
|
||||
names: lineProductsData.map((productLine) => (
|
||||
{
|
||||
name: productLine.productName,
|
||||
language: productLine.language
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(newProduct))
|
||||
}
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
if (languagesOption[0] !== undefined && lineProduct.language === '')
|
||||
lineProduct.language = languagesOption[0].key;
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.nameSelected === lineProduct.nameSelected);
|
||||
if (existingLine === -1) {
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
}
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewProduct = () => {
|
||||
addOrModifyLineProduct(
|
||||
{ nameSelected: "product" + lineProductsData.length, productName: "", language: "" }
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("newProduct")}</legend>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<label htmlFor="product">{t("product")}</label>
|
||||
<SubGroupSearchBox key="subGroupSelected" className='mbottom-10 width-100' nameSelect="subGroupSelected" onChange={(e) => setSelectedValue(e)} />
|
||||
<label htmlFor="CO2Emissions">{t("CO2Emissions")}</label>
|
||||
<input type="text" key="CO2Emissions" className='mbottom-10 width-100' onChange={(e) => setCO2Emission(e.target.value)} />
|
||||
<label htmlFor="WaterEmissions">{t("waterEmissions")}</label>
|
||||
<input type="text" key="waterEmissions" className='mbottom-10 width-100'onChange={(e) => setWaterEmission(e.target.value)} />
|
||||
<label htmlFor="amortization">{t("amortization")}</label>
|
||||
<input type="text" key="amortization" className='mbottom-10 width-100'onChange={(e) => setAmortization(e.target.value)} />
|
||||
{
|
||||
lineProductsData.map((item) => (
|
||||
<LineProductItem key={item.nameSelected} languageOptions={languagesOption} nameSelected={item.nameSelected} onChange={(e) => addOrModifyLineProduct(e)}/>
|
||||
))
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewProduct()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewTranslation")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewProductItem
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LineSubGroup = ({ nameSelect, languageOptions, onChange }) => {
|
||||
const [subGroup, setSubGroup] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
subGroup,
|
||||
language,
|
||||
nameSelect
|
||||
});
|
||||
}, [onChange, subGroup, language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="command">{t("subGroup")}</label>
|
||||
<input type="text" name="subgroup" id="subgroup" className='form-control width-100' value={subGroup} placeholder={t("subGroup")} onChange={(e) => setSubGroup(e.target.value)} />
|
||||
<label htmlFor="language">{t("language")}</label>
|
||||
<select name="language" id="language" value={language} className="form-control width-100" onChange={(e) => setLanguage(e.target.value)}>
|
||||
{languageOptions}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {reset, create} from '../../../features/subgroups/subGroupSlice'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { LineSubGroup } from './LineSubGroup'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {GroupSearchBox} from '../../SearchBox/GroupSearchBox'
|
||||
|
||||
export const NewSubGroup = ({ languagesOption }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.group)
|
||||
const [subgroups, setSubGroups] = useState([{key: 0, name: "subgroup0" }])
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
const [lineSubGroupsData, setLineSubGroupsData] = useState([])
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
setSubGroups([{key: 0, name: "subgroup0" }])
|
||||
setLineSubGroupsData([])
|
||||
toast.success(t("subGroupSuccess"))
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const newSubGroup = {
|
||||
group : selectedValue,
|
||||
names: lineSubGroupsData.map((subGroupLine) => (
|
||||
{
|
||||
name: subGroupLine.subGroup,
|
||||
language: subGroupLine.language
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(newSubGroup))
|
||||
}
|
||||
|
||||
const addOrModifyLineLanguage = (lineLanguage) => {
|
||||
if (languagesOption[0] !== undefined && lineLanguage.language === '')
|
||||
lineLanguage.language = languagesOption[0].key;
|
||||
let existingLine = lineSubGroupsData.findIndex(lpd => lpd.nameSelect === lineLanguage.nameSelect);
|
||||
if (existingLine === -1)
|
||||
setLineSubGroupsData(lineSubGroupsData.concat(lineLanguage))
|
||||
else {
|
||||
lineSubGroupsData[existingLine] = lineLanguage
|
||||
setLineSubGroupsData(lineSubGroupsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewGroup = () => {
|
||||
setSubGroups(subgroups.concat([
|
||||
{key: subgroups.length, name: "subgroup" + subgroups.length }
|
||||
]));
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("newSubGroup")}</legend>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<label htmlFor="group">{t("group")}</label>
|
||||
<GroupSearchBox key="groupSelected" className='mbottom-10' nameSelect="groupSelected" onChange={(e) => setSelectedValue(e)} />
|
||||
{
|
||||
subgroups.map((item) => (
|
||||
<LineSubGroup key={item.key} languageOptions={languagesOption} nameSelect={item.key} onChange={(e) => addOrModifyLineLanguage(e)}/>
|
||||
))
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewGroup()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewTranslation")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewSubGroup
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {createLaRuche, reset, resetIsSuccess} from '../../features/purchases/purchaseSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NewPurchaseLaRuche = ({ datePurchase, purchaseSubmittedForLaRuche }) => {
|
||||
const {isLoading, isError, isSuccess, message, purchasesToRename} = useSelector((state) => state.purchase)
|
||||
const [command, setCommand] = useState('')
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess && !isLoading) {
|
||||
if (purchasesToRename.length === 0) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
else {
|
||||
dispatch(resetIsSuccess())
|
||||
purchaseSubmittedForLaRuche(purchasesToRename)
|
||||
}
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchase = {
|
||||
datePurchase: datePurchase,
|
||||
command: command
|
||||
}
|
||||
|
||||
dispatch(createLaRuche(purchase))
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className="form-group">
|
||||
<label htmlFor="command">{t("command")}</label>
|
||||
<textarea name="command" id="command" className='form-control width-100' value={command} placeholder={t("command")} onChange={(e) => setCommand(e.target.value)}></textarea>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchaseLaRuche
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { reset, updatepurchase } from '../../features/purchases/purchaseSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LineProductUnknownPreFill } from '../SearchBox/LineProductUnknownPreFill';
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
export const NewPurchaseLaRucheUnknownProduct = ({ listUnknowProduct }) => {
|
||||
const {isLoading, isError, isSuccess, message, purchase } = useSelector((state) => state.purchase)
|
||||
const [lineProductsData, setLineProductsData] = useState([]);
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchaseToUpdate = {
|
||||
id: purchase.id,
|
||||
products: lineProductsData.flatMap((productLine) =>
|
||||
!Array.isArray(productLine.selectedValue) ?
|
||||
{
|
||||
product: productLine.selectedValue,
|
||||
quantity: productLine.quantity,
|
||||
price: productLine.price,
|
||||
currencyIsoCode: productLine.currencyIsoCode,
|
||||
translation: productLine.translation
|
||||
}
|
||||
: [])
|
||||
}
|
||||
|
||||
dispatch(updatepurchase(purchaseToUpdate))
|
||||
}
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.id === lineProduct.id);
|
||||
if (existingLine === -1)
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className="form-group" id="formProducts">
|
||||
<label>{t("product")}</label>
|
||||
{
|
||||
listUnknowProduct.map((item) => (
|
||||
<LineProductUnknownPreFill key={item.id} unknowProduct={item} nameSelect={item.id} onChange={(e) => addOrModifyLineProduct(e)}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchaseLaRucheUnknownProduct
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {create, reset} from '../../features/purchases/purchaseSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LineProduct } from '../../components/SearchBox/LineProduct';
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
export const NewPurchaseManual = ({ datePurchase, currency }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.purchase)
|
||||
const [products, setProducts] = useState([{key: "0", name: "product0"}]);
|
||||
const [lineProductsData, setLineProductsData] = useState([]);
|
||||
const [nbProduct, setNbProduct] = useState(1);
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
}, [isError, isSuccess, message, products, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchase = {
|
||||
datePurchase: datePurchase,
|
||||
products: lineProductsData.map((productLine) => (
|
||||
{
|
||||
product: productLine.selectedValue,
|
||||
quantity: productLine.quantity,
|
||||
price: productLine.price,
|
||||
currencyIsoCode: currency
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(purchase))
|
||||
}
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.nameSelect === lineProduct.nameSelect);
|
||||
if (existingLine === -1)
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewProduct = () => {
|
||||
setProducts(products.concat([
|
||||
{key: nbProduct, name: "product" + nbProduct }
|
||||
]));
|
||||
setNbProduct(nbProduct + 1)
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className="form-group" id="formProducts">
|
||||
<label>{t("product")}</label>
|
||||
{
|
||||
products.map((item) => (
|
||||
<LineProduct key={item.key} onChange={(e) => addOrModifyLineProduct(e)} nameSelect={item.name}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<a onClick={() => addNewProduct()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewProduct")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchaseManual
|
||||
19
frontend/src/components/NoteItem.jsx
Normal file
19
frontend/src/components/NoteItem.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
function NoteItem({id, note}) {
|
||||
const {user} = useSelector((state) => state.auth)
|
||||
|
||||
return (
|
||||
<div className="note" style={{
|
||||
backgroundColor: note.isStaff ? 'rgba(0,0,0,0.7)' : '#fff',
|
||||
color: note.isStaff ? '#fff' : '#000',
|
||||
}} id={id}>
|
||||
<h4>Note from {note.isStaff ? <span>Staff</span> : <span>{user.name}</span>}</h4>
|
||||
<p>{note.text}</p>
|
||||
<div className="note-date">{new Date(note.createdAt).toLocaleString('fr-FR')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoteItem;
|
||||
15
frontend/src/components/PrivateRoute.jsx
Normal file
15
frontend/src/components/PrivateRoute.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuthStatus } from "../hooks/useAuthStatus";
|
||||
import Spinner from './Spinner'
|
||||
|
||||
export const PrivateRoute = () => {
|
||||
const {loggedIn, checkingStatus} = useAuthStatus()
|
||||
|
||||
if (checkingStatus)
|
||||
return <Spinner />
|
||||
|
||||
return loggedIn ? <Outlet /> : <Navigate to='/login' />;
|
||||
};
|
||||
|
||||
export default PrivateRoute
|
||||
60
frontend/src/components/SearchBox/GroupSearchBox.jsx
Normal file
60
frontend/src/components/SearchBox/GroupSearchBox.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { reset, getgroupsbyname } from '../../features/groups/groupSlice';
|
||||
|
||||
export const GroupSearchBox = ({ nameSelect, onChange }) => {
|
||||
const { groups } = useSelector((state) => state.group)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState('');
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getgroupsbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(groups.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name
|
||||
};
|
||||
}));
|
||||
}, [groups]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("group")}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
35
frontend/src/components/SearchBox/LineProduct.jsx
Normal file
35
frontend/src/components/SearchBox/LineProduct.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProductSearchBox } from './ProductSearchBox';
|
||||
|
||||
export const LineProduct = ({ nameSelect, onChange }) => {
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const [quantity, setQuantity] = useState('');
|
||||
const [price, setPrice] = useState('');
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
nameSelect,
|
||||
quantity,
|
||||
price,
|
||||
selectedValue
|
||||
});
|
||||
}, [dispatch, onChange, quantity, selectedValue, price, nameSelect]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset className='mtop-10'>
|
||||
<legend>{t("product")}</legend>
|
||||
<div>
|
||||
<input type="text" name={`${nameSelect}quantity`} className="width-40 inlineflex form-control" id={`${nameSelect}quantity`} value={quantity} placeholder={t("quantity")} onChange={(e) => { setQuantity(e.target.value); }} />
|
||||
<input type="text" name={`${nameSelect}price`} className="width-40 inlineflex form-control" id={`${nameSelect}price`} value={price} placeholder={t("price")} onChange={(e) => { setPrice(e.target.value); }} />
|
||||
</div>
|
||||
<ProductSearchBox key={nameSelect} className='mbottom-10' nameSelect={nameSelect} onChange={(e) => setSelectedValue(e)}/>
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
48
frontend/src/components/SearchBox/LineProductPreFill.jsx
Normal file
48
frontend/src/components/SearchBox/LineProductPreFill.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProductSearchBoxPreFill } from './ProductSearchBoxPreFill';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
|
||||
export const LineProductPreFill = ({ product, nameSelect, onChange, toDelete }) => {
|
||||
const [selectedValue, setSelectedValue] = useState(product.productId);
|
||||
const [quantity, setQuantity] = useState(product.quantity);
|
||||
const [price, setPrice] = useState(product.price);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
id: nameSelect,
|
||||
quantity,
|
||||
price,
|
||||
selectedValue,
|
||||
translation: product.product,
|
||||
currencyIsoCode: product.currencyIsoCode
|
||||
});
|
||||
}, [dispatch, onChange, quantity, selectedValue, price, product.currencyIsoCode, product.product, nameSelect]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<div>
|
||||
<fieldset key={`${nameSelect}fieldset`} className='mtop-10'>
|
||||
<legend>{t("product")}</legend>
|
||||
<div>
|
||||
<input type="text" name={`${nameSelect}quantity`} className="width-40 inlineflex form-control" id={`${nameSelect}quantity`} value={quantity} placeholder={t("quantity")} onChange={(e) => { setQuantity(e.target.value); }} />
|
||||
<input type="text" name={`${nameSelect}price`} className="width-40 inlineflex form-control" id={`${nameSelect}price`} value={price} placeholder={t("price")} onChange={(e) => { setPrice(e.target.value); }} />
|
||||
</div>
|
||||
<input type="hidden" key={`${nameSelect}currency`} name={`${nameSelect}currency`} className="width-100 inlineflex form-control" id={`${nameSelect}currency`} onChange={() => {}} value={product.currencyIsoCode} />
|
||||
<ProductSearchBoxPreFill key={`${nameSelect}searchbox`} className='mbottom-10' preSelectedValue={product.productId} preSelectedInputValue={product.translation} nameSelect={nameSelect} onChange={(e) => setSelectedValue(e)}/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div>
|
||||
<a onClick={() => toDelete(product.id)} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaTrash />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProductSearchBox } from './ProductSearchBox';
|
||||
|
||||
export const LineProductUnknownPreFill = ({ unknowProduct, nameSelect, onChange }) => {
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const [quantity, setQuantity] = useState(unknowProduct.quantity);
|
||||
const [price, setPrice] = useState(unknowProduct.price);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
id: nameSelect,
|
||||
quantity,
|
||||
price,
|
||||
selectedValue,
|
||||
translation: unknowProduct.product,
|
||||
currencyIsoCode: unknowProduct.currencyIsoCode
|
||||
});
|
||||
}, [dispatch, onChange, quantity, selectedValue, price, unknowProduct.currencyIsoCode, unknowProduct.product, nameSelect]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset key={`${nameSelect}fieldset`} className='mtop-10'>
|
||||
<legend>{t("product")}</legend>
|
||||
<div>
|
||||
<input type="text" name={`${nameSelect}quantity`} className="width-40 inlineflex form-control" id={`${nameSelect}quantity`} value={quantity} placeholder={t("quantity")} onChange={(e) => { setQuantity(e.target.value); }} />
|
||||
<input type="text" name={`${nameSelect}price`} className="width-40 inlineflex form-control" id={`${nameSelect}price`} value={price} placeholder={t("price")} onChange={(e) => { setPrice(e.target.value); }} />
|
||||
</div>
|
||||
<input type="text" key={`${nameSelect}nameAlreadyPreFill`} name={`${nameSelect}nameAlreadyPreFill`} className="width-100 inlineflex form-control" id={`${nameSelect}nameAlreadyPreFill`} onChange={() => {}} value={unknowProduct.product} />
|
||||
<input type="hidden" key={`${nameSelect}currency`} name={`${nameSelect}currency`} className="width-100 inlineflex form-control" id={`${nameSelect}currency`} onChange={() => {}} value={unknowProduct.currencyIsoCode} />
|
||||
<ProductSearchBox key={`${nameSelect}searchbox`} className='mbottom-10' nameSelect={nameSelect} onChange={(e) => setSelectedValue(e)}/>
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
61
frontend/src/components/SearchBox/ProductSearchBox.jsx
Normal file
61
frontend/src/components/SearchBox/ProductSearchBox.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import {getproductbyname} from '../../features/products/productSlice'
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { reset } from '../../features/products/productSlice';
|
||||
|
||||
export const ProductSearchBox = ({ nameSelect, onChange }) => {
|
||||
const { products } = useSelector((state) => state.product)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState('');
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getproductbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(products.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name + ' - ' + p.group
|
||||
};
|
||||
}));
|
||||
}, [products]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("product")}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import {getproductbyname} from '../../features/products/productSlice'
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { reset } from '../../features/products/productSlice';
|
||||
|
||||
export const ProductSearchBoxPreFill = ({ preSelectedValue, preSelectedInputValue, nameSelect, onChange }) => {
|
||||
const { products } = useSelector((state) => state.product)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState(preSelectedInputValue);
|
||||
const [selectedValue, setSelectedValue] = useState(preSelectedValue);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getproductbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getproductbyname(inputValueChanged));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(products.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name + ' - ' + p.group
|
||||
};
|
||||
}));
|
||||
}, [products]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("product")}
|
||||
closeMenuOnSelect={true}
|
||||
defaultInputValue={preSelectedInputValue}
|
||||
defaultValue={preSelectedValue}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
60
frontend/src/components/SearchBox/SubGroupSearchBox.jsx
Normal file
60
frontend/src/components/SearchBox/SubGroupSearchBox.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {reset, getsubgroupsbyname} from '../../features/subgroups/subGroupSlice'
|
||||
|
||||
export const SubGroupSearchBox = ({ nameSelect, onChange }) => {
|
||||
const { subgroups } = useSelector((state) => state.subgroup)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState('');
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getsubgroupsbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(subgroups.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name + ' - ' + p.group
|
||||
};
|
||||
}));
|
||||
}, [subgroups]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("subGroup")}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
11
frontend/src/components/Spinner.jsx
Normal file
11
frontend/src/components/Spinner.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
function Spinner() {
|
||||
return (
|
||||
<div className='loadingSpinnerContainer'>
|
||||
<div className='loadingSpinner'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
17
frontend/src/components/TicketItem.jsx
Normal file
17
frontend/src/components/TicketItem.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
function TicketItem({id, ticket}) {
|
||||
return (
|
||||
<div className="ticket" id={id}>
|
||||
<div>{new Date(ticket.createdAt).toLocaleString('fr-FR')}</div>
|
||||
<div>{ticket.product}</div>
|
||||
<div className={`status status-${ticket.status}`}>
|
||||
{ticket.status}
|
||||
</div>
|
||||
<Link to={`/ticket/${ticket._id}`} className="btn btn-reverse btn-sm">View</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TicketItem;
|
||||
25
frontend/src/features/auth/authService.js
Normal file
25
frontend/src/features/auth/authService.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/user`;
|
||||
|
||||
//Register user
|
||||
const register = async(userData) => {
|
||||
await axios.post(`${API_URL}/register`, userData);
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
//Login user
|
||||
const login = async(userData) => {
|
||||
const response = await axios.post(`${API_URL}/login`, userData)
|
||||
if (response.data) {
|
||||
localStorage.setItem('user', JSON.stringify(response.data))
|
||||
}
|
||||
return response.data
|
||||
}
|
||||
|
||||
const authService = {register, logout, login}
|
||||
|
||||
export default authService
|
||||
94
frontend/src/features/auth/authSlice.js
Normal file
94
frontend/src/features/auth/authSlice.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import authService from "./authService";
|
||||
|
||||
const user = JSON.parse(localStorage.getItem('user'))
|
||||
|
||||
const initialState = {
|
||||
user: user ?? null,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: '',
|
||||
}
|
||||
|
||||
export const register = createAsyncThunk('auth/register', async(user, thunkAPI) => {
|
||||
try {
|
||||
return await authService.register(user)
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const logout = createAsyncThunk('auth/logout', async(thunkAPI) => {
|
||||
try {
|
||||
return await authService.logout()
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const login = createAsyncThunk('user/login', async(user, thunkAPI) => {
|
||||
try {
|
||||
return await authService.login(user)
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(register.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(register.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.user = action.payload
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(register.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.user = null
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(logout.fulfilled, (state) => {
|
||||
state.user = null
|
||||
})
|
||||
.addCase(login.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(login.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.user = action.payload
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(login.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
state.user = null
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const {reset} = authSlice.actions
|
||||
|
||||
export default authSlice.reducer
|
||||
38
frontend/src/features/currencies/currencyService.js
Normal file
38
frontend/src/features/currencies/currencyService.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/currency`;
|
||||
|
||||
//Create currency
|
||||
const create = async(currencyData, token) => {
|
||||
const response = await axios.post(API_URL, currencyData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all currencies
|
||||
const getcurrencies = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a currency
|
||||
const getcurrency = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a currency
|
||||
const closecurrency = async(id, token) => {
|
||||
const response = await axios.put(`${API_URL}/${id}`, {status: 'close'}, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const currencyService = {create, getcurrencies, getcurrency, closecurrency}
|
||||
|
||||
export default currencyService
|
||||
132
frontend/src/features/currencies/currencySlice.js
Normal file
132
frontend/src/features/currencies/currencySlice.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import currencieservice from "./currencyService";
|
||||
|
||||
const initialState = {
|
||||
currency: {},
|
||||
currencies: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: '',
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('currencies/create', async(currency, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.create(currency, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getcurrencies = createAsyncThunk('currencies/getcurrencies', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.getcurrencies(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getcurrency = createAsyncThunk('currencies/getcurrency', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.getcurrency(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const closecurrency = createAsyncThunk('currencies/closecurrency', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.closecurrency(thunkAPI.getState().currency.currency._id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const currencySlice = createSlice({
|
||||
name: 'currency',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.currency = {}
|
||||
state.currencies = []
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getcurrencies.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getcurrencies.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.currencies = action.payload
|
||||
})
|
||||
.addCase(getcurrencies.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getcurrency.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getcurrency.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.currency = action.payload
|
||||
})
|
||||
.addCase(getcurrency.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(closecurrency.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(closecurrency.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.currencies.map((currency) => currency._id === action.payload._id ? action.payload.status = 'closed' : currency)
|
||||
})
|
||||
.addCase(closecurrency.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
export const {reset} = currencySlice.actions
|
||||
|
||||
export default currencySlice.reducer
|
||||
50
frontend/src/features/groups/groupService.js
Normal file
50
frontend/src/features/groups/groupService.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/group`;
|
||||
|
||||
//Create group
|
||||
const create = async(groupData, token) => {
|
||||
const response = await axios.post(API_URL, groupData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all groups
|
||||
const getgroups = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getgroupsbyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a group
|
||||
const getgroup = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a group
|
||||
const updategroup = async(group, token) => {
|
||||
const response = await axios.put(API_URL, group, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a group
|
||||
const deletegroup = async(groupId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/${groupId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const groupService = {create, getgroups, getgroup, getgroupsbyname, updategroup, deletegroup}
|
||||
|
||||
export default groupService
|
||||
164
frontend/src/features/groups/groupSlice.js
Normal file
164
frontend/src/features/groups/groupSlice.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import groupservice from "./groupService";
|
||||
|
||||
const initialState = {
|
||||
group: {},
|
||||
groups: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('groups/create', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.create(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getgroups = createAsyncThunk('groups/getgroups', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.getgroups(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getgroupsbyname = createAsyncThunk('groups/getgroupsbyname', async(name, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.getgroupsbyname(name, getLanguage(thunkAPI), getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getgroup = createAsyncThunk('groups/getgroup', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.getgroup(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updategroup = createAsyncThunk('groups/update', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.updategroup(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletegroup = createAsyncThunk('groups/delete', async(groupId, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.deletegroup(groupId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const groupslice = createSlice({
|
||||
name: 'group',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.group = {}
|
||||
state.CO2Emissions = 0
|
||||
state.groups = []
|
||||
state.groupsToRename = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getgroups.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getgroups.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.groups = action.payload
|
||||
})
|
||||
.addCase(getgroups.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getgroupsbyname.fulfilled, (state, action) => {
|
||||
state.groups = action.payload
|
||||
})
|
||||
.addCase(getgroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getgroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.group = action.payload
|
||||
})
|
||||
.addCase(getgroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updategroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updategroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updategroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
const getLanguage = () => {
|
||||
return localStorage.getItem('i18nextLng');
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = groupslice.actions
|
||||
|
||||
export default groupslice.reducer
|
||||
44
frontend/src/features/languages/languageService.js
Normal file
44
frontend/src/features/languages/languageService.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/language`;
|
||||
|
||||
//Create language
|
||||
const create = async(languageData, token) => {
|
||||
const response = await axios.post(API_URL, languageData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all languages
|
||||
const getlanguages = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a language
|
||||
const getlanguageById = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a language
|
||||
const getlanguagebyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a language
|
||||
const closelanguage = async(id, token) => {
|
||||
const response = await axios.put(`${API_URL}/${id}`, {status: 'close'}, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const languageService = {create, getlanguages, getlanguageById, getlanguagebyname, closelanguage}
|
||||
|
||||
export default languageService
|
||||
147
frontend/src/features/languages/languageSlice.js
Normal file
147
frontend/src/features/languages/languageSlice.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import languageservice from "./languageService";
|
||||
|
||||
const initialState = {
|
||||
language: {},
|
||||
languages: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('languages/create', async(language, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.create(language, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getlanguages = createAsyncThunk('languages/getlanguages', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.getlanguages(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getlanguage = createAsyncThunk('languages/getlanguage', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.getlanguage(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updatelanguage = createAsyncThunk('languages/update', async(language, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.updatelanguage(language, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletelanguage = createAsyncThunk('languages/delete', async(languageId, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.deletelanguage(languageId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const languageslice = createSlice({
|
||||
name: 'language',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.language = {}
|
||||
state.languages = []
|
||||
state.groupsToRename = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getlanguages.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getlanguages.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.languages = action.payload
|
||||
})
|
||||
.addCase(getlanguages.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getlanguage.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getlanguage.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.language = action.payload
|
||||
})
|
||||
.addCase(getlanguage.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updatelanguage.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updatelanguage.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updatelanguage.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = languageslice.actions
|
||||
|
||||
export default languageslice.reducer
|
||||
44
frontend/src/features/products/productService.js
Normal file
44
frontend/src/features/products/productService.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/product`;
|
||||
|
||||
//Create product
|
||||
const create = async(productData, token) => {
|
||||
const response = await axios.post(API_URL, productData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all products
|
||||
const getproducts = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getproduct = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getproductbyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a product
|
||||
const closeproduct = async(id, token) => {
|
||||
const response = await axios.put(`${API_URL}/${id}`, {status: 'close'}, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const productService = {create, getproducts, getproduct, getproductbyname, closeproduct}
|
||||
|
||||
export default productService
|
||||
160
frontend/src/features/products/productSlice.js
Normal file
160
frontend/src/features/products/productSlice.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import productService from "./productService";
|
||||
|
||||
const initialState = {
|
||||
product: {},
|
||||
products: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: '',
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('products/create', async(product, thunkAPI) => {
|
||||
try {
|
||||
return await productService.create(product, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getproducts = createAsyncThunk('products/getproducts', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await productService.getproducts(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getproduct = createAsyncThunk('products/getproduct', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await productService.getproduct(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getproductbyname = createAsyncThunk('products/getproductbyname', async(name, thunkAPI) => {
|
||||
try {
|
||||
return await productService.getproductbyname(name, getLanguage(thunkAPI), getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const closeproduct = createAsyncThunk('products/closeproduct', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await productService.closeproduct(thunkAPI.getState().product.product._id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const productSlice = createSlice({
|
||||
name: 'product',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.product = {}
|
||||
state.products = []
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getproductbyname.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getproductbyname.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.products = action.payload
|
||||
})
|
||||
.addCase(getproductbyname.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getproducts.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getproducts.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.products = action.payload
|
||||
})
|
||||
.addCase(getproducts.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getproduct.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getproduct.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.product = action.payload
|
||||
})
|
||||
.addCase(getproduct.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(closeproduct.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(closeproduct.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.products.map((product) => product._id === action.payload._id ? action.payload.status = 'closed' : product)
|
||||
})
|
||||
.addCase(closeproduct.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
const getLanguage = () => {
|
||||
return localStorage.getItem('i18nextLng');
|
||||
}
|
||||
|
||||
export const {reset} = productSlice.actions
|
||||
|
||||
export default productSlice.reducer
|
||||
68
frontend/src/features/purchases/purchaseService.js
Normal file
68
frontend/src/features/purchases/purchaseService.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/purchase`;
|
||||
|
||||
//Create purchase
|
||||
const create = async(purchaseData, token) => {
|
||||
const response = await axios.post(API_URL, purchaseData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Create purchase La Ruche
|
||||
const createLaRuche = async(purchaseData, token) => {
|
||||
const response = await axios.post(`${API_URL}/laruche`, purchaseData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all purchases
|
||||
const getpurchases = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all purchases by date
|
||||
const getpurchasesbydate = async(dates, token) => {
|
||||
const response = await axios.get(`${API_URL}/${dates.startDate}/${dates.endDate}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get CO2 emissions between two dates
|
||||
const getCO2bydate = async(dates, token) => {
|
||||
const response = await axios.get(`${API_URL}/CO2/${dates.startDate}/${dates.endDate}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a purchase
|
||||
const getpurchase = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a purchase
|
||||
const updatepurchase = async(purchase, token) => {
|
||||
const response = await axios.put(API_URL, purchase, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a purchase
|
||||
const deletepurchase = async(purchaseId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/${purchaseId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a purchase
|
||||
const deletelinepurchase = async(linePurchaseId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/line/${linePurchaseId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const purchaseService = {create, createLaRuche, getpurchases, getpurchasesbydate, getCO2bydate, getpurchase, updatepurchase, deletepurchase, deletelinepurchase}
|
||||
|
||||
export default purchaseService
|
||||
232
frontend/src/features/purchases/purchaseSlice.js
Normal file
232
frontend/src/features/purchases/purchaseSlice.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import purchaseService from "./purchaseService";
|
||||
|
||||
const initialState = {
|
||||
purchase: {},
|
||||
purchases: [],
|
||||
CO2Emissions: 0,
|
||||
purchasesToRename: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('purchases/create', async(purchase, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.create(purchase, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const createLaRuche = createAsyncThunk('purchases/createLaRuche', async(purchase, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.createLaRuche(purchase, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getpurchases = createAsyncThunk('purchases/getpurchases', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getpurchases(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getpurchasesbydate = createAsyncThunk('purchases/getpurchasebydate', async(dates, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getpurchasesbydate(dates, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getCO2bydate = createAsyncThunk('purchases/getCO2bydate', async(dates, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getCO2bydate(dates, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getpurchase = createAsyncThunk('purchases/getpurchase', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getpurchase(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updatepurchase = createAsyncThunk('purchases/update', async(purchase, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.updatepurchase(purchase, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletepurchase = createAsyncThunk('purchases/delete', async(purchaseId, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.deletepurchase(purchaseId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletelinepurchase = createAsyncThunk('purchases/deletelinepurchase', async(linePurchaseId, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.deletelinepurchase(linePurchaseId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const purchaseSlice = createSlice({
|
||||
name: 'purchase',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.purchase = {}
|
||||
state.CO2Emissions = 0
|
||||
state.purchases = []
|
||||
state.purchasesToRename = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(createLaRuche.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(createLaRuche.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchasesToRename = action.payload.productsToTranslate
|
||||
state.purchase = action.payload
|
||||
})
|
||||
.addCase(createLaRuche.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getpurchases.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getpurchases.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchases = action.payload
|
||||
})
|
||||
.addCase(getpurchases.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getpurchasesbydate.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getpurchasesbydate.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchases = action.payload
|
||||
})
|
||||
.addCase(getpurchasesbydate.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getCO2bydate.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getCO2bydate.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.CO2Emissions = action.payload
|
||||
})
|
||||
.addCase(getCO2bydate.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getpurchase.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getpurchase.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchase = action.payload
|
||||
})
|
||||
.addCase(getpurchase.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updatepurchase.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updatepurchase.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updatepurchase.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = purchaseSlice.actions
|
||||
|
||||
export default purchaseSlice.reducer
|
||||
50
frontend/src/features/subgroups/subGroupService.js
Normal file
50
frontend/src/features/subgroups/subGroupService.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/subGroup`;
|
||||
|
||||
//Create group
|
||||
const create = async(subGroupData, token) => {
|
||||
const response = await axios.post(API_URL, subGroupData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all groups
|
||||
const getsubgroups = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getsubgroupsbyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a group
|
||||
const getsubgroup = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a group
|
||||
const updatesubgroup = async(subGroup, token) => {
|
||||
const response = await axios.put(API_URL, subGroup, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a group
|
||||
const deletesubgroup = async(subGroupId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/${subGroupId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const groupService = {create, getsubgroups, getsubgroup, getsubgroupsbyname, updatesubgroup, deletesubgroup}
|
||||
|
||||
export default groupService
|
||||
162
frontend/src/features/subgroups/subGroupSlice.js
Normal file
162
frontend/src/features/subgroups/subGroupSlice.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import subGroupService from "./subGroupService";
|
||||
|
||||
const initialState = {
|
||||
subgroup: {},
|
||||
subgroups: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('subgroups/create', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.create(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getsubgroups = createAsyncThunk('subgroups/getsubgroups', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.getsubgroups(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getsubgroupsbyname = createAsyncThunk('subgroups/getsubgroupsbyname', async(name, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.getsubgroupsbyname(name, getLanguage(thunkAPI), getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getsubgroup = createAsyncThunk('subgroups/subgetgroup', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.getsubgroup(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updatesubgroup = createAsyncThunk('subgroups/update', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.updatesubgroup(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletesubgroup = createAsyncThunk('subgroups/delete', async(groupId, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.deletesubgroup(groupId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const subgroupslice = createSlice({
|
||||
name: 'group',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.subgroup = {}
|
||||
state.subgroups = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getsubgroups.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getsubgroups.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.subgroups = action.payload
|
||||
})
|
||||
.addCase(getsubgroups.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getsubgroupsbyname.fulfilled, (state, action) => {
|
||||
state.subgroups = action.payload
|
||||
})
|
||||
.addCase(getsubgroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getsubgroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.subgroup = action.payload
|
||||
})
|
||||
.addCase(getsubgroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updatesubgroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updatesubgroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updatesubgroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
const getLanguage = () => {
|
||||
return localStorage.getItem('i18nextLng');
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = subgroupslice.actions
|
||||
|
||||
export default subgroupslice.reducer
|
||||
11
frontend/src/hooks/setupProxy.js
Normal file
11
frontend/src/hooks/setupProxy.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = function(app) {
|
||||
app.use(
|
||||
'/backendApi',
|
||||
createProxyMiddleware({
|
||||
target: process.env.REACT_APP_API_URL,
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
22
frontend/src/hooks/useAuthStatus.js
Normal file
22
frontend/src/hooks/useAuthStatus.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
|
||||
export const useAuthStatus = () => {
|
||||
const [loggedIn, setLoggedIn] = useState(false)
|
||||
const [checkingStatus, setCheckingStatus] = useState(true)
|
||||
|
||||
const {user} = useSelector((state) => state.auth)
|
||||
|
||||
useEffect(() => {
|
||||
if (user && new Date() < new Date(user.validTo)) {
|
||||
setLoggedIn(true)
|
||||
}
|
||||
else {
|
||||
setLoggedIn(false)
|
||||
}
|
||||
setCheckingStatus(false)
|
||||
}, [user])
|
||||
|
||||
return {loggedIn, checkingStatus}
|
||||
}
|
||||
30
frontend/src/i18n.js
Normal file
30
frontend/src/i18n.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import i18n from "i18next";
|
||||
import Backend from "i18next-xhr-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n
|
||||
// load translation using xhr -> see /public/locales
|
||||
// learn more: https://github.com/i18next/i18next-xhr-backend
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||
.use(LanguageDetector)
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: "FR",
|
||||
lng: 'FR',
|
||||
debug: true,
|
||||
/* can have multiple namespace, in case you want to divide a huge translation into smaller pieces and load them on demand */
|
||||
ns: ["Header"],
|
||||
defaultNS: "Header",
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react as it escapes by default
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
317
frontend/src/index.css
Normal file
317
frontend/src/index.css
Normal file
@@ -0,0 +1,317 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.header ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header ul li {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.header ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header ul li a:hover {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.header ul li a svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 50px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.heading p {
|
||||
color: #828282;
|
||||
}
|
||||
|
||||
.boxes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.boxes div {
|
||||
padding: 30px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.boxes h2 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.boxes a:hover {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
padding: 10px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.width-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.width-80 {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.width-70 {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.width-60 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.width-40 {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.width-20 {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.width-10 {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.mtop-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mbottom-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inlineflex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
text-align: left;
|
||||
display: block;
|
||||
margin: 0 0 5px 3px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
appearance: button;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.btn-reverse {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: darkred;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
width: 150px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #000;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
color: red;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
p.status-in-progress {
|
||||
color: orangered;
|
||||
}
|
||||
|
||||
p.status-waiting {
|
||||
color: red;
|
||||
}
|
||||
|
||||
p.status-ready {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
p.status-complete {
|
||||
color: green;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: sticky;
|
||||
top: 95vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadingSpinnerContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 5000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 8px solid;
|
||||
border-color: #000 transparent #555 transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.boxes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.ticket-created h2,
|
||||
.heading h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.heading p {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
26
frontend/src/index.js
Normal file
26
frontend/src/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { store } from './app/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import Spinner from './components/Spinner'
|
||||
import "./i18n";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
25
frontend/src/pages/Home.jsx
Normal file
25
frontend/src/pages/Home.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {HomePurchase} from '../components/Home/HomePurchase'
|
||||
import { reset} from '../features/purchases/purchaseSlice'
|
||||
|
||||
function Home() {
|
||||
const { t } = useTranslation(["Home"]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return <>
|
||||
<Link onClick={() => dispatch(reset())} to='/new-purchase'className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("createNewPurchase")}
|
||||
</Link>
|
||||
<Link onClick={() => dispatch(reset())} to='/new-product' className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("createNewProduct")}
|
||||
</Link>
|
||||
<HomePurchase />
|
||||
</>;
|
||||
}
|
||||
|
||||
export default Home;
|
||||
84
frontend/src/pages/Login.jsx
Normal file
84
frontend/src/pages/Login.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { FaSignInAlt } from 'react-icons/fa'
|
||||
import { toast } from 'react-toastify'
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import {login, reset } from '../features/auth/authSlice'
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Login() {
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const {email, password} = formData
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {user, isLoading, isSuccess, isError, message} = useSelector((state) => state.auth)
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess && user)
|
||||
navigate('/')
|
||||
dispatch(reset())
|
||||
}, [user, isLoading, isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = async(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const userData = {
|
||||
email,
|
||||
password
|
||||
}
|
||||
|
||||
await dispatch(login(userData))
|
||||
|
||||
if (!isLoading && isSuccess)
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
const onChange = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id] : e.target.value,
|
||||
}))
|
||||
}
|
||||
|
||||
const { t } = useTranslation(["Login"]);
|
||||
|
||||
if (isLoading)
|
||||
return (<div>
|
||||
Loading...
|
||||
</div>)
|
||||
|
||||
return (<>
|
||||
<section className="heading">
|
||||
<h1>
|
||||
<FaSignInAlt /> {t("login")}
|
||||
</h1>
|
||||
<p>{t("fillLogin")}</p>
|
||||
</section>
|
||||
<section className="form">
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<input type="email" name="email" id="email" value={email} className="form-control width-100" placeholder={t("emailPlaceholder")} onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="password" name="password" id="password" value={password} className="form-control width-100" placeholder={t("passwordPlaceholder")} onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
44
frontend/src/pages/NewProduct.jsx
Normal file
44
frontend/src/pages/NewProduct.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import Spinner from '../components/Spinner'
|
||||
import BackButton from '../components/BackButton'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import NewGroup from '../components/NewProduct/NewGroup/NewGroup';
|
||||
import NewSubGroup from '../components/NewProduct/NewSubGroup/NewSubGroup';
|
||||
import NewProductItem from '../components/NewProduct/NewProductItem/NewProductItem';
|
||||
import { getlanguages } from '../features/languages/languageSlice';
|
||||
|
||||
function NewProduct() {
|
||||
const { languages, isLoading } = useSelector((state) => state.language)
|
||||
const [languagesOption, setLanguageOptions] = useState([])
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
setLanguageOptions(languages.map((languageOption) => {
|
||||
if (languageOption.names.length > 0) {
|
||||
return <option value={languageOption.id} key={languageOption.id}>{languageOption.names.findIndex(function(name) { return name.language === i18n.language }) === -1 ? languageOption.names[0].name : languageOption.names.find(function(name) { return name.language === i18n.language }).name}</option>;
|
||||
}
|
||||
}));
|
||||
}, [languages, i18n.language])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getlanguages())
|
||||
}, [dispatch])
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<BackButton url='/' />
|
||||
<NewGroup languagesOption={languagesOption} />
|
||||
<NewSubGroup languagesOption={languagesOption} />
|
||||
<NewProductItem languagesOption={languagesOption} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewProduct
|
||||
97
frontend/src/pages/NewPurchase.jsx
Normal file
97
frontend/src/pages/NewPurchase.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { reset} from '../features/purchases/purchaseSlice'
|
||||
import {getcurrencies} from '../features/currencies/currencySlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../components/Spinner'
|
||||
import BackButton from '../components/BackButton'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NewPurchaseManual } from '../components/NewPurchaseTypes/NewPurchaseManual';
|
||||
import { NewPurchaseLaRuche } from '../components/NewPurchaseTypes/NewPurchaseLaRuche';
|
||||
import { NewPurchaseLaRucheUnknownProduct } from '../components/NewPurchaseTypes/NewPurchaseLaRucheUnknownProduct';
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
function NewPurchase() {
|
||||
const { currencies, isLoading, isError, message } = useSelector((state) => state.currency)
|
||||
const [typeOfPurchase, setTypeOfPurchase] = useState('manual')
|
||||
const [datePurchase, setDatePurchase] = useState(new Date())
|
||||
const [currency, setCurrency] = useState('EUR');
|
||||
const [currencyOptions, setCurrencyOptions] = useState([]);
|
||||
const [laRucheUnknownProduct, setLaRucheUnknownProduct] = useState([]);
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
setCurrencyOptions(currencies.map((currencyOption) => {
|
||||
if (currencyOption.isoCode === 'EUR')
|
||||
setCurrency(currencyOption.id)
|
||||
return <option value={currencyOption.id} key={currencyOption.id}>{currencyOption.names.find(function(name) { return name.language === i18n.language }).name}</option>;
|
||||
}));
|
||||
}, [currencies])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(reset())
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
}, [isError])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getcurrencies())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [isLoading])
|
||||
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<BackButton url='/' />
|
||||
<section className="heading">
|
||||
<h1>{t("title")}</h1>
|
||||
</section>
|
||||
<section className="form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">{t("typeOfPurchase")}</label>
|
||||
<select name="typeOfPurchase" id="typeOfPurchase" value={typeOfPurchase} className="form-control width-100" onChange={(e) => setTypeOfPurchase(e.target.value)}>
|
||||
<option value="manual">{t("manual")}</option>
|
||||
<option value="LaRuche">La Ruche Qui Dit Oui</option>
|
||||
</select>
|
||||
<label htmlFor="name">{t("datePurchase")}</label>
|
||||
<DatePicker dateFormat="dd/MM/yyyy" selected={datePurchase} className="form-control width-100" onChange={(date) => setDatePurchase(date)} />
|
||||
{ typeOfPurchase === "manual" ?
|
||||
(
|
||||
<>
|
||||
<label htmlFor="currency">{t("currency")}</label>
|
||||
<select name="currency" id="currency" value={currency} className="form-control width-100" onChange={(e) => setCurrency(e.target.value)}>
|
||||
{currencyOptions}
|
||||
</select>
|
||||
</>
|
||||
) :
|
||||
<></>
|
||||
}
|
||||
</div>
|
||||
{ typeOfPurchase === "manual" ?
|
||||
(
|
||||
<NewPurchaseManual datePurchase={datePurchase} currency={currency} />
|
||||
) :
|
||||
laRucheUnknownProduct.length === 0 ?
|
||||
(
|
||||
<NewPurchaseLaRuche datePurchase={datePurchase} purchaseSubmittedForLaRuche={(e) => setLaRucheUnknownProduct(e)} />
|
||||
) :
|
||||
(
|
||||
<NewPurchaseLaRucheUnknownProduct listUnknowProduct={laRucheUnknownProduct} />
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchase
|
||||
86
frontend/src/pages/NewTicket.jsx
Normal file
86
frontend/src/pages/NewTicket.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {create, reset} from '../features/tickets/ticketSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../components/Spinner'
|
||||
import BackButton from '../components/BackButton'
|
||||
|
||||
function NewTicket() {
|
||||
const {user} = useSelector((state) => state.auth)
|
||||
const {isLoading, isError, isSuccess, message} = useSelector((state) => state.ticket)
|
||||
const [name] = useState(user.name)
|
||||
const [email] = useState(user.email)
|
||||
const [product, setProduct] = useState('iPhone')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
dispatch(reset())
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const ticket = {
|
||||
user: user.id,
|
||||
product,
|
||||
description
|
||||
}
|
||||
dispatch(create(ticket))
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackButton url='/' />
|
||||
<section className="heading">
|
||||
<h1>Create new ticket</h1>
|
||||
<p>Please fill out the form below</p>
|
||||
</section>
|
||||
<section className="form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Customer name</label>
|
||||
<input type="text" className="form-control" value={name} disabled />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Customer email</label>
|
||||
<input type="text" className="form-control" value={email} disabled />
|
||||
</div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="product">Product</label>
|
||||
<select name="product" id="product" value={product} className="form-control" onChange={(e) => setProduct(e.target.value)}>
|
||||
<option value="iPhone">iPhone</option>
|
||||
<option value="MacBook Pro">MacBook Pro</option>
|
||||
<option value="iMac">iMac</option>
|
||||
<option value="iPad">iPad</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description of the issue</label>
|
||||
<textarea name="description" id="description" className='form-control' value={description} placeholder="Description" onChange={(e) => setDescription(e.target.value)}></textarea>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewTicket
|
||||
163
frontend/src/pages/Purchase.jsx
Normal file
163
frontend/src/pages/Purchase.jsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import {useDispatch, useSelector} from 'react-redux'
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {toast} from 'react-toastify'
|
||||
import Modal from 'react-modal'
|
||||
import Spinner from "../components/Spinner";
|
||||
import { LineProductPreFill } from "../components/SearchBox/LineProductPreFill";
|
||||
import { getpurchase, reset, updatepurchase, deletelinepurchase} from '../features/purchases/purchaseSlice'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { FaShoppingCart } from 'react-icons/fa';
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
function Purchase() {
|
||||
const {purchase, isLoading, isError, message} = useSelector((state) => state.purchase)
|
||||
const dispatch = useDispatch()
|
||||
const params = useParams()
|
||||
const [purchaseUser, setPurchaseUser] = useState()
|
||||
const [datePurchase, setDatePurchase] = useState()
|
||||
const [lineProductsData, setLineProductsData] = useState([]);
|
||||
const { t } = useTranslation(["Purchase"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
toast.error(message)
|
||||
}
|
||||
}, [dispatch, isError, message])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getpurchase(params.id))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setPurchaseUser(purchase)
|
||||
if (Object.keys(purchase).length > 0) {
|
||||
if (lineProductsData.length == 0) {
|
||||
setDatePurchase(new Date(Date.parse(purchase.datePurchase)))
|
||||
setLineProductsData(purchase.products.map((p) => {
|
||||
return {
|
||||
id : p.id,
|
||||
currencyIsoCode : p.currencyIsoCode,
|
||||
productId : p.productId,
|
||||
price : p.price,
|
||||
quantity : p.quantity,
|
||||
translation : p.product,
|
||||
selectedValue : p.id
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, [purchase])
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.id === lineProduct.id);
|
||||
if (existingLine === -1)
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteLineProduct = (p) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.id === p);
|
||||
dispatch(reset());
|
||||
if (isNaN(p)) {
|
||||
dispatch(deletelinepurchase(p))
|
||||
}
|
||||
lineProductsData.splice(existingLine, 1)
|
||||
}
|
||||
|
||||
const addNewProduct = () => {
|
||||
setLineProductsData(lineProductsData.concat([
|
||||
{
|
||||
id : lineProductsData.length,
|
||||
currencyIsoCode : lineProductsData[0].currencyIsoCode,
|
||||
productId : '',
|
||||
price : 0,
|
||||
quantity : 0,
|
||||
translation : '',
|
||||
selectedValue : ''
|
||||
}
|
||||
]));
|
||||
}
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchaseToUpdate = {
|
||||
id: purchase.id,
|
||||
products: lineProductsData.flatMap((productLine) =>
|
||||
!Array.isArray(productLine.selectedValue) ?
|
||||
{
|
||||
id: productLine.id,
|
||||
product: productLine.selectedValue,
|
||||
quantity: productLine.quantity,
|
||||
price: productLine.price,
|
||||
currencyIsoCode: productLine.currencyIsoCode,
|
||||
translation: productLine.translation
|
||||
}
|
||||
: [])
|
||||
}
|
||||
|
||||
dispatch(updatepurchase(purchaseToUpdate))
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
if (isError)
|
||||
return <h3>An error occured</h3>
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
Object.keys(purchase).length > 0 ? (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className='width-100'>
|
||||
<div className='inlineflex'>
|
||||
<div className='width-40'>{t("datePurchase")}</div>
|
||||
<div className='width-60'><DatePicker dateFormat="dd/MM/yyyy" selected={datePurchase} className="form-control width-100" onChange={(date) => setDatePurchase(date)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='width-100'>
|
||||
<div className='inlineflex'>
|
||||
<div className='width-80'>{t("CO2Cost")}</div>
|
||||
<div className='width-20'>{parseFloat(purchaseUser.cO2Cost).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='width-100'>
|
||||
<div className='inlineflex'>
|
||||
<div className='width-80'>{t("WaterCost")}</div>
|
||||
<div className='width-20'>{parseFloat(purchaseUser.waterCost).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
lineProductsData.length > 0 ?
|
||||
lineProductsData.map((item) => (
|
||||
<LineProductPreFill key={item.id} product={item} nameSelect={item.id} onChange={(e) => addOrModifyLineProduct(e)} toDelete={(e) => deleteLineProduct(e)}/>
|
||||
)) :
|
||||
<></>
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewProduct()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewProduct")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : <></>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Purchase;
|
||||
88
frontend/src/pages/Register.jsx
Normal file
88
frontend/src/pages/Register.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
import { useState, useEffect } from "react";
|
||||
import { FaUser } from 'react-icons/fa'
|
||||
import { toast } from 'react-toastify'
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import {register, reset} from '../features/auth/authSlice'
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Register() {
|
||||
const { i18n } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
})
|
||||
|
||||
const {email, password, password2} = formData
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {user, isLoading, isError, isSuccess, message} = useSelector(state => state.auth)
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess && user) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
}, [user, isLoading, isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = async(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (password !== password2){
|
||||
toast.error('Passwords must match')
|
||||
}
|
||||
else {
|
||||
const userData = {
|
||||
email,
|
||||
password,
|
||||
language: i18n.language
|
||||
}
|
||||
dispatch(register(userData));
|
||||
if (!isLoading && isSuccess)
|
||||
navigate('/')
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id] : e.target.value,
|
||||
}))
|
||||
}
|
||||
|
||||
return (<>
|
||||
<section className="heading">
|
||||
<h1>
|
||||
<FaUser /> Register {user}
|
||||
</h1>
|
||||
<p>Please create an account</p>
|
||||
</section>
|
||||
<section className="form">
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<input type="email" name="email" id="email" value={email} className="form-control width-100" placeholder="Enter your email" onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="password" name="password" id="password" value={password} className="form-control width-100" placeholder="Enter your password" onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="password" name="password2" id="password2" value={password2} className="form-control width-100" placeholder="Re-enter your password" onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default Register;
|
||||
137
frontend/src/serviceWorker.js
Normal file
137
frontend/src/serviceWorker.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user