Project files

This commit is contained in:
2023-11-09 18:47:11 +01:00
parent 695abe054b
commit c415135aae
8554 changed files with 858111 additions and 0 deletions

23
reactFrontend/.gitignore vendored Normal file
View File

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

36
reactFrontend/README.md Normal file
View File

@@ -0,0 +1,36 @@
## Project Overview
This project is a web application built using JavaScript, npm, yarn, and React. It is designed to provide users with an interface to view and manage purchases, articles, and analyses.
## Project Structure
The project is structured as follows:
- `App.js`: This is the main entry point of the application. It imports and renders the necessary components and sets up the routing.
- `components/`: This directory contains all the components used in the application. Each component is a separate file and is responsible for rendering a specific part of the UI.
- `App.css`: This file contains the styles used in the application.
- `MyRoutes.js`: This file contains the routing configuration for the application.
## Dependencies
The project uses the following dependencies:
- `react`: A JavaScript library for building user interfaces.
- `react-router-dom`: A library for routing in React applications.
- `@mui/material`: A library of React components that implement Google's Material Design.
## Getting Started
To get started with the project, follow these steps:
1. Clone the repository to your local machine.
2. Install the necessary dependencies using `npm` or `yarn`.
3. Run the application using `npm start` or `yarn start`.

View File

@@ -0,0 +1,57 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@devexpress/dx-react-core": "^3.0.2",
"@devexpress/dx-react-grid": "^3.0.2",
"@devexpress/dx-react-grid-material-ui": "^3.0.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.3",
"@mui/lab": "^5.0.0-alpha.139",
"@mui/material": "^5.14.4",
"@mui/x-data-grid": "^6.11.1",
"@mui/x-date-pickers": "^6.11.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"axios": "^1.4.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.9",
"graphql": "^16.7.1",
"plotly.js": "^2.25.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-dropzone": "^12.0.5",
"react-plotly.js": "^2.6.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -0,0 +1,54 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@devexpress/dx-react-core": "^3.0.2",
"@devexpress/dx-react-grid": "^3.0.2",
"@devexpress/dx-react-grid-material-ui": "^3.0.2",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.6.1",
"@mui/lab": "^5.0.0-alpha.86",
"@mui/material": "^5.6.1",
"@mui/x-data-grid": "^5.9.0",
"@mui/x-date-pickers": "^6.11.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.0.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.26.1",
"date-fns": "^2.28.0",
"graphql": "^16.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-dropzone": "^12.0.5",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

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

View File

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

38
reactFrontend/src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

54
reactFrontend/src/App.js Normal file
View File

@@ -0,0 +1,54 @@
import React from 'react';
import './App.css';
import { BrowserRouter, Routes, Route } from "react-router-dom";
import {Box, Stack} from '@mui/material';
import Navbar from "./components/Navbar";
import Sidebar from "./components/Sidebar";
//import Feed from "./components/Feed";
import PurchaseDetails from "./components/PurchaseDetails.component";
import ArticleDetails from "./components/ArticleDetails.component";
import ArticleOverview from "./components/ArticleOverview.component";
import PurchaseOverview from "./components/PurchaseOverview.component";
import AnalysesOverview from "./components/AnalysesOverview.component";
//import MyRoutes from './MyRoutes';
const App = () => {
return (
<Box>
{/* Set up the browser router */}
<BrowserRouter>
{/* Render the navbar */}
<Navbar />
{/* Set up a stack layout with a sidebar and content */}
<Stack direction="row" spacing={2} justifyContent="space-between">
{/* Render the sidebar */}
<Sidebar />
{/* Set up the routes */}
<Routes>
{/* Set up the route for the purchase overview */}
<Route path="/" element={<PurchaseOverview />} exact/>
{/* Set up the route for the purchase details */}
<Route path="/PurchaseDetails" element={<PurchaseDetails />} />
{/* Set up the route for the article details */}
<Route path="/ArticleDetails" element={<ArticleDetails />} />
{/* Set up the route for the article overview */}
<Route path="/Articles" element={<ArticleOverview />} />
{/* Set up the route for the analyses overview */}
<Route path="/Analyses" element={<AnalysesOverview />} />
</Routes>
</Stack>
</BrowserRouter>
</Box>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,23 @@
import React from 'react'
import { BrowserRouter, Routes, Route } from "react-router-dom";
import PurchaseDetails from "./components/PurchaseDetails.component";
import ArticleDetails from "./components/ArticleDetails.component";
import ArticleOverview from "./components/ArticleOverview.component";
import Feed from "./components/Feed";
const MyRoutes = (props) => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Feed />} exact/>
<Route path="/PurchaseDetails" element={<PurchaseDetails />} />
<Route path="/ArticleDetails" element={<ArticleDetails />} />
<Route path="/Articles" element={<ArticleOverview />} />
</Routes>
</BrowserRouter>
)
}
export default MyRoutes

View File

@@ -0,0 +1,221 @@
import * as React from 'react';
import {useState, useEffect} from 'react'
import dayjs from 'dayjs';
//import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import { Button, Paper, styled, } from '@mui/material'
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import Plot from 'react-plotly.js';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
const baseURL = "http://localhost:8000/api"
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`vertical-tabpanel-${index}`}
aria-labelledby={`vertical-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};
function a11yProps(index) {
return {
id: `vertical-tab-${index}`,
'aria-controls': `vertical-tabpanel-${index}`,
};
}
const AnalysesOverview = (props) => {
const navigate = useNavigate();
const [tableData, setTableData] = useState([{article_id: -1, count: 0}]);
const [fromDate, setFromDate] = useState(dayjs('2023-01-01'));
const [toDate, setToDate] = useState(dayjs('2023-09-01'));
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
useEffect(()=>{
},[])
const handlePlotButton = async () => {
const data = {fromDate: fromDate.format("YYYY-MM-DD"), toDate: toDate.format("YYYY-MM-DD")};
console.log(data);
const receivedData = await axios.post(baseURL+"/analyses/countArticles", {
data: data,
})
setTableData(receivedData.data)
}
const handleBrandPlotButton = async () => {
const data = {fromDate: fromDate.format("YYYY-MM-DD"), toDate: toDate.format("YYYY-MM-DD")};
console.log(data);
const receivedData = await axios.post(baseURL+"/analyses/countBrands", {
data: data,
})
setTableData(receivedData.data)
}
return (
<div style={{ height: 800, width: '100%' }}>
<Typography variant="h2" gutterBottom>
Übersicht über alle Analysen
</Typography>
<Box
sx={{ flexGrow: 1, bgcolor: 'background.paper', display: 'flex', height: 500 }} >
<Tabs
orientation="vertical"
variant="scrollable"
value={value}
onChange={handleChange}
aria-label="Vertical tabs example"
sx={{ borderRight: 1, borderColor: 'divider' }}
>
<Tab label="Barplot: Häufigkeit Artikel" {...a11yProps(0)} />
<Tab label="Barplot: Häufigkeit Marke" {...a11yProps(1)} />
<Tab label="Item Three" {...a11yProps(2)} />
<Tab label="Item Four" {...a11yProps(3)} />
<Tab label="Item Five" {...a11yProps(4)} />
<Tab label="Item Six" {...a11yProps(5)} />
<Tab label="Item Seven" {...a11yProps(6)} />
</Tabs>
<TabPanel value={value} index={0}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Start Datum"
format="DD.MM.YYYY"
value={dayjs(fromDate)}
onChange={val => {setFromDate(val)}}
/>
<DatePicker
label="End Datum"
format="DD.MM.YYYY"
value={dayjs(toDate)}
onChange={val => {setToDate(val)}}
/>
<Button variant="contained"
onClick={handlePlotButton}
>
Plotten
</Button>
</LocalizationProvider>
<Plot
data={[
{
type: 'bar',
y: Array.from(Array(tableData.length).keys()),
x: tableData.map( (val) => (val.count) ),
orientation: 'h'
},
]}
layout={ {height: 3500, title: 'Häufigkeit Artikel',
yaxis: {
tickvals: Array.from(Array(tableData.length).keys()),
ticktext: tableData.map( (val) => (val.article_name) ),
automargin: true,
}}
}
/>
</TabPanel>
<TabPanel value={value} index={1}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Start Datum"
format="DD.MM.YYYY"
value={dayjs(fromDate)}
onChange={val => {setFromDate(val)}}
/>
<DatePicker
label="End Datum"
format="DD.MM.YYYY"
value={dayjs(toDate)}
onChange={val => {setToDate(val)}}
/>
<Button variant="contained"
onClick={handleBrandPlotButton}
>
Plotten
</Button>
</LocalizationProvider>
<Plot
data={[
{
type: 'bar',
y: Array.from(Array(tableData.length).keys()),
x: tableData.map( (val) => (val.count) ),
orientation: 'h'
},
]}
layout={ {height: 3500, title: 'Häufigkeit Marken',
yaxis: {
tickvals: Array.from(Array(tableData.length).keys()),
ticktext: tableData.map( (val) => (val.brand_name) ),
automargin: true,
}}
}
/>
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
<TabPanel value={value} index={3}>
Item Four
</TabPanel>
<TabPanel value={value} index={4}>
Item Five
</TabPanel>
<TabPanel value={value} index={5}>
Item Six
</TabPanel>
<TabPanel value={value} index={6}>
Item Seven
</TabPanel>
</Box>
</div>
);
}
export default AnalysesOverview

View File

@@ -0,0 +1,535 @@
import {useState, useEffect} from 'react'
import React from 'react'
import axios from 'axios';
import { useLocation } from 'react-router-dom';
import { Grid, Typography, TextField, Select, MenuItem, Stack} from '@mui/material'
import { Button, Paper, styled, } from '@mui/material'
import Box from '@mui/material/Box';
import { grey } from '@mui/material/colors';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
//import ReceipeCanvas from "./Canvas.component"
import CategoryTree from "./CategoryTree.component"
import ListFieldApi from "./ListFieldApi.component"
import PackagingDataGrid2 from "./PackagingDataGrid2.component"
import NutriDataGrid from "./NutriDataGrid.component"
import PictureCard from "./pictureCard.component"
import IngredienceDataGrid from "./IngredienceDataGrid.component"
const baseURL = "http://localhost:8000/api"
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: 'center',
color: theme.palette.text.secondary,
}));
//article is pushed from PurchaseDetail.component
const ArticleDetails = (props) => {
const location = useLocation();
var earlyExit = false;
if (location.state != null){
console.log(location)
console.log(location.state.id)
console.log(location.state.article_id)
}
else {
console.log("No data to show");
console.log(location)
location.state = {'article_id': {'name': '','EAN':'', 'brand':''}};
console.log(location)
earlyExit = true;
}
const units = ['kg','g','l','ml','St.','kJ','kcal','µg'];
const grades = ['','A','B','C','D','E'];
const novas = ['','1','2','3','4'];
const [article, setArticle] = useState(location.state.article_id);
const [name, setName] = useState(article.name);
const [EAN, setEAN] = useState(article.EAN);
const [offlink, setOfflink] = useState(article.offlink);
const [quantityPerPackage, setQuantityPerPackage] = useState(article.quantityPerPackage);
const [unit, setUnit] = useState(article.unit);
//const [nutritionGrade, setNutritionGrade] = useState(article.nutritionGrade);
//const [novaGroup, setNovaGroup] = useState(article.novaGroup);
//const [ecoGrade, setEcoGrade] = useState(article.ecoGrade);
const [snackbar, setSnackbar] = useState(null);
const EANvalid = () => {
if (EAN === null)
return false;
else {
if (EAN.length === 13 || EAN.length === 8) {
return true;
} else {
return false;
}
}
}
const handleSearchButton = async () => {
try {
const ans = await axios.get(baseURL+"/offsearch_get_by_ean/"+EAN+"/"+article.pk, { });
//setArticle(ans.data.article);
const newArticle = ans.data;
setArticle(newArticle);
//console.log('New Article:')
//console.log(newArticle)
location.state.article_id = newArticle;
setSnackbar({ children: 'Daten erfolgreich übernommen', severity: 'success' });
}
catch(error){
setSnackbar({ children: error.message, severity: 'error' });
}
};
const handelArticleChange = (value) => {
setArticle(value);
}
const handleArticleChangeAPI = async (event, field, value) => {
try {
if (field === 'nutritionGrade' || field === 'novaGroup' || field === 'ecoGrade'){
console.log(value)
const newArticle = { ...article, [field]: value};
const sendArticle = await axios.put(baseURL+"/article/"+article.pk, {
article: newArticle
});
setArticle(newArticle);
}
else {
const newArticle = { ...article, [field]: value};
const sendArticle = await axios.put(baseURL+"/article/"+article.pk, {
article: newArticle
});
setArticle(newArticle);
}
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
}
catch(error){
setSnackbar({ children: error.message, severity: 'error' });
setName(eval('article.'+field));
}
}
const handleCloseSnackbar = () => setSnackbar(null);
if (earlyExit){
return(<div> </div>);
}
return (
<Grid container spacing={1}>
<Grid item xs={12}>
<Typography variant="h3" component="div" gutterBottom>
{article.brand === null ? '': article.brand.name} {" "+ article.name}
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
margin="dense"
id="nameField"
//defaultValue={name != null ? name : ""}
value={name}
onChange={(e) => setName(e.target.value)}
error={(article.name.length === 0)}
label="Name"
type="text"
variant="standard"
onBlur={(e) => handleArticleChangeAPI(e,'name', e.target.value)}
/>
</Grid>
<Grid item xs={12}>
<TextField
//autoFocus
fullWidth
margin="dense"
id="EAN"
value={EAN != null ? EAN : "" }
onChange={(e) => setEAN(e.target.value)}
error={!EANvalid()}
label="EAN/EAN-13"
type="number"
variant="standard"
onBlur={(e) => handleArticleChangeAPI(e,'EAN', e.target.value)}
/>
</Grid>
<Grid item xs={12}>
<TextField
//autoFocus
fullWidth
margin="dense"
id="offlink"
value={offlink != null ? offlink : ""}
onChange={(e) => setOfflink(e.target.value)}
label="OpenFoodFact Link"
type="text"
variant="standard"
onBlur={(e) => handleArticleChangeAPI(e,'offlink', e.target.value)}
/>
</Grid>
<Grid item xs={4}>
<Button variant="contained"
onClick={handleSearchButton}
disabled={!EANvalid()}
>
EAN bei OpenFoodFact suchen
</Button>
</Grid>
{/*-----------------------------------------------------------------------------------*/}
{/*-----------------------------------------------------------------------------------*/}
<Grid item xs={12}>
<Typography variant="h4" component="div" gutterBottom textAlign="center">
Produkteigenschaften
</Typography>
</Grid>
<Grid item xs={4}>
<PictureCard
type="Artikelbild"
article={article}
pictureType="frontImage"
onChange={handelArticleChange}
>
</PictureCard>
</Grid>
<Grid item xs={8}>
<Grid container spacing={1}>
<Grid item xs={12}>
<ListFieldApi
apiURL="/article/brands/"
titleName="Marke"
addName="Marke"
multiple={false}
value={article.brand}
onChange={handelArticleChange}
article={article}
fieldName={'brand'}
/>
</Grid>
<Grid item xs={10}>
<TextField
//autoFocus
fullWidth
margin="dense"
id="quantityPerPackage"
value={quantityPerPackage != null ? quantityPerPackage : ""}
onChange={(e) => setQuantityPerPackage(e.target.value)}
label="Mengenangabe"
type="number"
variant="standard"
onBlur={(e) => handleArticleChangeAPI(e,'quantityPerPackage',e.target.value)}
/>
</Grid>
<Grid item xs={2}>
<Select
fullWidth
id="unit"
value={unit}
label="Einheit"
onChange={(e) => {setUnit(e.target.value);handleArticleChangeAPI(e,'unit',e.target.value)}}
>
{units.map((unit) => (
<MenuItem
key={unit}
value={unit}
//style={getStyles(name, personName, theme)}
>
{unit}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle1" component="div" gutterBottom>
Verpackung
</Typography>
</Grid>
<Grid item xs={12}>
<PackagingDataGrid2
articleId={article.pk}
/>
</Grid>
<Grid item xs={12}>
<ListFieldApi
apiURL="/article/labels/"
titleName="Kennzeichnungen"
addName="Label"
multiple={true}
value={article.labels}
onChange={handelArticleChange}
article={article}
fieldName={'labels'}
/>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle1" component="div" gutterBottom>
Kategorie
</Typography>
</Grid>
<Grid item xs={12}>
<Box sx={{ borderRadius: '10px', p: 2, border: '1px solid', borderColor: grey[300] }}>
<CategoryTree
value={article.category}
onChange={handelArticleChange}
article={article}
/>
</Box>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle1" component="div" gutterBottom>
Scors
</Typography>
</Grid>
<Grid item xs={12}>
<Stack direction="row" spacing={2}>
<Item>
<Select
fullWidth
labelId="nutriSelect"
id="nutri"
value={article.nutritionGrade === null ? '' : article.nutritionGrade}
label="Nutri-Score"
onChange={(e) => {const value = e.target.value;
//setNutritionGrade(typeof value === 'string' ? value.split(',') : value);
handleArticleChangeAPI(e,'nutritionGrade', value)
}
}
>
{grades.map((grade) => (
<MenuItem
key={grade}
value={grade}
//style={getStyles(name, personName, theme)}
>
{grade}
</MenuItem>
))}
</Select>
<img width={'100%'} src={"http://127.0.0.1:8080/Nutri-score-"+article.nutritionGrade+".svg"} alt="NutriScore"/>
</Item>
<Item>
<Select
fullWidth
labelId="nutriSelect"
id="nova"
value={article.novaGroup === null ? '' : article.novaGroup}
label="NovaGroup"
onChange={(e) => {const value = e.target.value;
//setNovaGroup(typeof value === 'string' ? value.split(',') : value);
handleArticleChangeAPI(e,'novaGroup', value)
}
}
>
{novas.map((grade) => (
<MenuItem
key={grade}
value={grade}
//style={getStyles(name, personName, theme)}
>
{grade}
</MenuItem>
))}
</Select>
<img width={'100%'} src={"http://127.0.0.1:8080/nova-group-"+article.novaGroup+".svg"} alt="NovaGroup"/>
</Item>
<Item>
<Select
fullWidth
labelId="nutriSelect"
id="eco"
value={article.ecoGrade === null ? '' : article.ecoGrade}
label="EcoGrade"
onChange={(e) => {const value = e.target.value;
//setEcoGrade(typeof value === 'string' ? value.split(',') : value);
handleArticleChangeAPI(e,'ecoGrade', value)
}
}
>
{grades.map((grade) => (
<MenuItem
key={grade}
value={grade}
//style={getStyles(name, personName, theme)}
>
{grade}
</MenuItem>
))}
</Select>
<img width={'100%'} src={"http://127.0.0.1:8080/Eco-score-"+article.ecoGrade+".svg"} alt="EcoGrade"/>
</Item>
</Stack>
</Grid>
</Grid>
</Grid>
{/*-----------------------------------------------------------------------------------*/}
{/*-----------------------------------------------------------------------------------*/}
<Grid item xs={12}>
<Typography variant="h4" component="div" gutterBottom textAlign="center">
Inhaltsstoffe
</Typography>
</Grid>
<Grid item xs={4}>
<PictureCard
type="Bild der Inhaltsstoffe"
article={article}
pictureType="ingredientsImage"
onChange={handelArticleChange}
>
</PictureCard>
</Grid>
<Grid item xs={8}>
<Grid item xs={12}>
{/*
<ListFieldApi
apiURL="/article/ingrediences"
titleName="Inhaltsstoffe"
addName="Zutat"
multiple={true}
value={article.ingredients}
onChange={handelArticleChange}
article={article}
fieldName={'ingredients'}
/>
*/}
<IngredienceDataGrid
articleId={article.pk}
/>
</Grid>
<Grid item xs={12}>
<ListFieldApi
apiURL="/article/allergenes/"
titleName="Allergene"
addName="Zutat"
multiple={true}
value={article.allergenes}
onChange={handelArticleChange}
article={article}
fieldName={'allergenes'}
/>
</Grid>
<Grid item xs={12}>
<ListFieldApi
apiURL="/article/traces/"
titleName="Spuren"
addName="Zutat"
multiple={true}
value={article.traces}
onChange={handelArticleChange}
article={article}
fieldName={'traces'}
/>
</Grid>
<Grid item xs={12}>
<ListFieldApi
apiURL="/article/embs/"
titleName="EMB Nummern"
addName="Nummer"
multiple={true}
value={article.embs}
onChange={handelArticleChange}
article={article}
fieldName={'embs'}
/>
</Grid>
<Grid item xs={12}>
<ListFieldApi
apiURL="/article/manufacturingPlaces/"
titleName="Ort der Herstellung"
addName="Ort"
multiple={true}
value={article.manufacturingPlaces}
onChange={handelArticleChange}
article={article}
fieldName={'manufacturingPlaces'}
/>
</Grid>
<Grid item xs={12}>
<ListFieldApi
apiURL="/article/originIngredients/"
titleName="Herkunft der Inhaltsstoffe"
addName="Ort"
multiple={true}
value={article.originIngredients}
onChange={handelArticleChange}
article={article}
fieldName={'originIngredients'}
/>
</Grid>
</Grid>
{/*-----------------------------------------------------------------------------------*/}
{/*-----------------------------------------------------------------------------------*/}
<Grid item xs={12}>
<Typography variant="h4" component="div" gutterBottom textAlign="center">
Nährwertangaben
</Typography>
</Grid>
<Grid item xs={4}>
<PictureCard
type="Bild der Nährwerte"
article={article}
pictureType="nutritionImage"
onChange={handelArticleChange}
>
</PictureCard>
</Grid>
<Grid item xs={8}>
<NutriDataGrid
article={article}
/>
</Grid>
<div>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
</Grid>
)
}
export default ArticleDetails

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import {useState, useEffect} from 'react'
//import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
//import Box from '@mui/material/Box';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import ArticleCard from "./articleCard.component";
const baseURL = "http://localhost:8000/api"
const ArticelOverview = (props) => {
const navigate = useNavigate();
const [tableData, setTableData] = useState([]);
useEffect(()=>{
axios.get(baseURL+"/articles/")
.then((res) => {
const data = res.data;
console.log(data)
setTableData(data);
})
},[])
return (
<div style={{ height: 800, width: '100%' }}>
<Typography variant="h2" gutterBottom>
Übersicht über alle Artikel
</Typography>
<Container maxWidth="xl">
<Grid
container
direction="row"
justify="space-evenly"
alignItems="stretch"
spacing={2}
>
{
tableData.map(data => (
<Grid item
key={`GridItem-${data.pk}`} xs={2}
>
<ArticleCard
article={data}
navigate={navigate}
/>
</Grid>
))
}
</Grid>
</Container>
</div>
);
}
export default ArticelOverview

View File

@@ -0,0 +1,193 @@
import React from 'react'
import { useRef, useEffect, useState } from 'react'
const ReceipeCanvas = props => {
const canvasRef = useRef(null)
const contextRef = useRef(null);
const { image, myRef, setNewRect, ...rest } = props
const [isDrawing, setIsDrawing] = useState(false);
//const [data, setData] = useState(drawData)
const canvasOffSetX = useRef(null);
const canvasOffSetY = useRef(null);
const startX = useRef(null);
const startY = useRef(null);
const rectWidth = useRef(null);
const rectHeight = useRef(null);
const imgWidth = useRef(null);
const imgHeight = useRef(null);
var ratio;
const draw = (drawData) => {
console.log('draw')
//contextRef.current.beginPath();
contextRef.current.strokeStyle="red";
//contextRef.current.rect(drawData.location_x*ratio,drawData.location_y*ratio,drawData.location_w*ratio,drawData.location_h*ratio);
//contextRef.current.stroke();
const hRatio = contextRef.current.canvas.width / image.width;
//Clears the entire canvas
contextRef.current.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
//Redraw the background image
contextRef.current.drawImage(image, 0,0,
image.width,
image.height,
0,0,
image.width*hRatio,
image.height*hRatio);
contextRef.current.strokeRect(drawData.location_x*hRatio,drawData.location_y*hRatio,drawData.location_w*hRatio,drawData.location_h*hRatio);
}
const drawImage = async (image) => {
console.log('Draw image fkt')
/*
var img = new Image();
img.onload = function(){
imgWidth.current = img.naturalWidth
imgHeight.current = img.naturalHeight;
//console.log("width,height:", img.width, img.height);
//console.log("offsetW,offsetH:", img.offsetWidth, img.offsetHeight);
}
img.src = image.src;
*/
await image.decode()
contextRef.current.clearRect(0, 0, contextRef.current.canvas.width, contextRef.current.canvas.height);
ratio = contextRef.current.canvas.width / image.width;
contextRef.current.canvas.height = image.height*ratio;
contextRef.current.drawImage(image, 0,0,
image.width,
image.height,
0,0,
image.width*ratio,
image.height*ratio);
console.log(image.width, ratio, contextRef.current.canvas.height)
}
myRef.current.draw = draw
myRef.current.drawImage = drawImage
useEffect(() => {
const canvas = canvasRef.current
const context = canvas.getContext('2d')
context.canvas.width = 300
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
ratio = context.canvas.width / image.width ;
context.canvas.height = image.height*ratio;
context.drawImage(image, 0,0,
image.width,
image.height,
0,0,
image.width*ratio,
image.height*ratio);
//draw(context,hRatio);
contextRef.current = context;
console.log('useeffect',image)
},[image])
const startDrawingRectangle = ({nativeEvent}) => {
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
const canvasOffSet = canvasRef.current.getBoundingClientRect();
canvasOffSetX.current = canvasOffSet.left;
canvasOffSetY.current = canvasOffSet.top;
startX.current = nativeEvent.clientX - canvasOffSetX.current;
startY.current = nativeEvent.clientY - canvasOffSetY.current;
console.log('#nativ',nativeEvent.clientX,nativeEvent.clientY)
setIsDrawing(true);
console.log("Start drawing")
};
const drawRectangle = ({nativeEvent}) => {
if (!isDrawing) {
return;
}
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
const newMouseX = nativeEvent.clientX - canvasOffSetX.current;
const newMouseY = nativeEvent.clientY - canvasOffSetY.current;
rectWidth.current = newMouseX - startX.current;
rectHeight.current = newMouseY - startY.current;
contextRef.current.strokeStyle="red";
//Clears the entire canvas
contextRef.current.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
//Redraw the background image
const hRatio = contextRef.current.canvas.width / image.width;
contextRef.current.drawImage(image, 0,0,
image.width,
image.height,
0,0,
image.width*hRatio,
image.height*hRatio);
contextRef.current.strokeRect(startX.current, startY.current, rectWidth.current, rectHeight.current);
};
const stopDrawingRectangle = ({nativeEvent}) => {
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
const hRatio = contextRef.current.canvas.width / image.width;
console.log({location_x: startX.current/hRatio, location_y: startY.current/hRatio, location_w: rectWidth.current/hRatio,location_h: rectHeight.current/hRatio})
if (startX.current/hRatio < 0){
startX.current = 0;
}
if ((rectWidth.current < 0) && (rectHeight.current > 0)) {
setNewRect({location_x: (startX.current+rectWidth.current)/hRatio, location_y: startY.current/hRatio, location_w: -rectWidth.current/hRatio,location_h: rectHeight.current/hRatio});
}
else if ((rectWidth.current > 0) && (rectHeight.current < 0)) {
setNewRect({location_x: startX.current/hRatio, location_y: (startY.current+rectHeight.current)/hRatio, location_w: rectWidth.current/hRatio,location_h: -rectHeight.current/hRatio});
}
else if ((rectHeight.current < 0) && (rectWidth.current < 0)) {
console.log('<<')
setNewRect({location_x: (startX.current+rectWidth.current)/hRatio, location_y: (startY.current+rectHeight.current)/hRatio, location_w: -rectWidth.current/hRatio,location_h: -rectHeight.current/hRatio});
}
else {
setNewRect({location_x: startX.current/hRatio, location_y: startY.current/hRatio, location_w: rectWidth.current/hRatio,location_h: rectHeight.current/hRatio});
}
setIsDrawing(false);
};
return <canvas
ref={canvasRef}
onMouseDown={startDrawingRectangle}
onMouseMove={drawRectangle}
onMouseUp={stopDrawingRectangle}
//onMouseLeave={stopDrawingRectangle}
{...rest}>
</canvas>
}
ReceipeCanvas.defaultProps = {
myRef: {current: {}}
}
export default ReceipeCanvas

View File

@@ -0,0 +1,94 @@
import {useState, useEffect} from 'react'
import React from 'react'
import axios from 'axios';
import {Snackbar, Alert} from '@mui/material';
import TreeView from '@mui/lab/TreeView';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import TreeItem from '@mui/lab/TreeItem';
const baseURL = "http://localhost:8000/api"
const apiURL = "/article/categories/"
const CategoryTree = (props) => {
const { value, article, onChange } = props;
const [categories, setCategories] = useState({id: 'root', data: {id: -1, name: ''}});
const [snackbar, setSnackbar] = useState(null);
const renderTree = (nodes) => (
<TreeItem key={nodes.id} nodeId={nodes.id.toString()} label={nodes.data.name}>
{Array.isArray(nodes.children)
? nodes.children.map((node) => renderTree(node))
: null}
</TreeItem>
);
const handleCloseSnackbar = () => setSnackbar(null);
const handleNodeSelect = async (e, nodeid) => {
try {
//var completeCategory = {};
const completeCategory = await axios.get(baseURL+"/article/categories/"+nodeid);
//console.log(completeCategory.data);
const newArticle = { ...article, 'category': completeCategory.data};
const sendArticle = await axios.put(baseURL+"/article/"+article.pk, {
article: newArticle
});
//console.log(newArticle)
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
onChange(newArticle);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
};
useEffect(()=>{
axios.get(baseURL+apiURL)
.then((res) => {
const data = res.data;
setCategories(data);
//console.log(data);
})
},[])
return (
<div style={{ height: 350, width: '100%' }}>
<TreeView
//aria-label="rich object"
defaultExpanded={value === null ? [] : value.ancestorIds.map(x => String(x)) }
defaultExpandIcon={<ChevronRightIcon />}
defaultCollapseIcon={<ExpandMoreIcon />}
defaultSelected={value === null ? [''] : value.id.toString()}
onNodeSelect={handleNodeSelect}
sx={{ height: 350, flexGrow: 1, overflowY: 'auto' }}
>
{renderTree(categories)}
</TreeView>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
);
}
export default CategoryTree

View File

@@ -0,0 +1,17 @@
import React, { Component } from "react";
export default class Home extends Component {
render() {
return (
<div className="Home">
<div className="lander">
<h1>Home page</h1>
<p>A simple app showing react button click navigation</p>
<form>
</form>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,362 @@
import {useState, useEffect} from 'react'
import React from 'react'
import axios from 'axios';
import PropTypes from 'prop-types';
//import { useNavigate, useLocation } from 'react-router-dom';
import { Button } from '@mui/material'
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import { DataGrid, GridToolbarContainer, GridActionsCellItem } from '@mui/x-data-grid';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import Grid from '@mui/material/Grid';
import Dialog from '@mui/material/Dialog';
import TextField from '@mui/material/TextField';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import ListFieldApiwoArticle from "./ListFieldApiwoArticle.component"
const baseURL = "http://localhost:8000/api"
const apiURL = "/article/ingrediences/"
function EditToolbar(props) {
const { setTableData, articleId, setOpenAddDialog } = props;
const handleNewEntryClick = async () => {
const newValue = { ingredient: {id: -1, name: 'Bitte auswählen'}, percent: null, isNew: true, article: articleId };
const receivedData = await axios.post(baseURL+"/article/articleIngrediences/"+articleId, {
newValue,
})
//console.log(receivedData)
const newRow = { id: receivedData.data.id, ingredient: {id: -1, name: 'Bitte auswählen'}, percent: receivedData.data.percent, isNew: true, article: receivedData.data.article, position: receivedData.data.position };
setTableData((oldRows) => [...oldRows, newRow]);
};
const handleNewIngredientClick = () => {
setOpenAddDialog(true);
/*
const newValue = { packaging_id: {id: -1, name: 'Bitte auswählen'}, weight: 0, isNew: true, article_id: articleId };
const receivedData = await axios.post(baseURL+"/article/articleIngrediences/"+articleId, {
newValue,
})
//console.log(receivedData)
const newRow = { id: receivedData.data.id, ingredient: {id: -1, name: 'Bitte auswählen'}, percent: receivedData.data.percent, isNew: true, article: receivedData.data.article, position: receivedData.data.positio };
setTableData((oldRows) => [...oldRows, newRow]);
*/
};
return (
<GridToolbarContainer>
<Button color="primary" startIcon={<AddIcon />} onClick={handleNewEntryClick}>
Neuer Eintrag
</Button>
<Button color="primary" startIcon={<AddIcon />} onClick={handleNewIngredientClick}>
Neue Zutat
</Button>
</GridToolbarContainer>
);
}
EditToolbar.propTypes = {
setTableData: PropTypes.func.isRequired,
setOpenAddDialog: PropTypes.func.isRequired,
}
const IngredienceDataGrid = (props) => {
const { articleId } = props;
//list contains all ingrediences
const [list, setList] = useState([{id: -1, name: '', subIngredients: []}]);
const [tableData, setTableData] = useState([{id: -1, ingredient: {id: -1, name: ''}, percent: null, article: -1, position: -1}]);
const [snackbar, setSnackbar] = useState(null);
const [openAddDialog, setOpenAddDialog] = useState(false);
const [newIngredient, setNewIngredient] = useState({name: '', allergenes: [], vegan: false, additive: false, vegetarian: false, palmoilfree: true, subIngredients: [], eNumber: ''});
const columnsIngrediences = [
{
field: 'position',
headerName: 'Pos.',
width: 50,
editable: true,
},
{
field: 'ingredient',
headerName: 'Name',
flex: 1,
type: 'singleSelect',
valueOptions: list,
getOptionValue: (value) => value.id,
getOptionLabel: (value) => {if (value.subIngredients.length === 0) {
return value.name
} else {
return value.name + ' ('+value.subIngredients.map(x => x.name).join(', ')+')'
}
},
editable: true,
valueGetter: (params) => {try {return params.row.ingredient.id;} catch {return '';} },
},
{
field: 'percent',
headerName: 'Anteil an Gesamtmasse',
type: 'number',
width:170,
editable: true,
valueFormatter: (params) => {
if (params.value == null) {
return '';
}
return `${params.value} %`;
},
},
{
field: 'actions',
type: 'actions',
headerName: 'Aktionen',
width: 100,
cellClassName: 'actions',
getActions: ({ id }) => {
return [
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={(e) => handleDeleteClick(id,e)}
color="inherit"
/>,
];
},
}
]
const findId = (idToLookFor) => {
for (var i = 0; i < list.length; i++) {
if (list[i].name === idToLookFor) {
return(list[i].id);
}
}
}
const handleCloseSnackbar = () => setSnackbar(null);
//Anscheinend ist es notwendig das event e zu übergeben, ansonsten passieren komische Dinge (kein async, multiple trigger beim rendern etc)
const handleDeleteClick = async (id,e) => {
const newData = tableData.filter((row) => row.id !== id);
//console.log('Delete clicked')
//console.log(id)
//console.log(e)
try {
const receivedData = await axios.delete(baseURL+"/article/articleIngrediences/"+articleId, {
data: id,
})
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setTableData(newData);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
};
const processRowUpdate = React.useCallback( async (newRow, oldRow) => {
console.log(newRow)
const ingredient = list.find(x => x.id === newRow.ingredient);
const updatedRow = {'id': newRow.id, 'ingredient': ingredient, 'position': newRow.position, 'isNew': false, 'article': articleId, 'percent': newRow.percent};
const newData = tableData.map((row) => (row.id === newRow.id ? updatedRow : row));
try {
const response = await axios.put(baseURL+"/article/articleIngrediences/"+articleId, {
updatedRow
});
//console.log(response);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setTableData(newData);
//onChange(newData);
return updatedRow;
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
return oldRow;
}
}
);
//const handleProcessRowUpdateError = React.useCallback((error) => {
// setSnackbar({ children: error.message, severity: 'error' });
//}, []);
const handleClose = () => {
}
const handleAdd = async() => {
console.log(newIngredient)
try {
const ans = await axios.post(baseURL+"/article/ingrediences/", {
newIngredient,
})
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setOpenAddDialog(false);
setNewIngredient({name: '', allergenes: [], vegan: false, additive: false, vegetarian: false, palmoilfree: true, subIngredients: [], eNumber: ''});
setList(list.concat(ans.data));
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
}
useEffect(()=>{
axios.get(baseURL+apiURL)
.then((res) => {
const data = res.data;
console.log(data);
setList(data);
})
axios.get(baseURL+"/article/articleIngrediences/"+articleId)
.then((res) => {
const data = res.data;
//Add id for each element for displaying in Data Grid
//data.forEach((item, i) => {
// item.id = i + 1;
//});
setTableData(data);
console.log("Fetching ingrediences data")
console.log(data)
})
},[])
return (
<div style={{ height: 350, width: '100%' }}>
<DataGrid
rows={tableData}
columns={columnsIngrediences}
pageSize={10}
editMode="row"
//getRowId={(row) => row.internalId}
processRowUpdate={processRowUpdate}
//onProcessRowUpdateError={handleProcessRowUpdateError}
experimentalFeatures={{ newEditingApi: true }}
components={{
Toolbar: EditToolbar,
}}
componentsProps={{
toolbar: { setTableData, articleId, setOpenAddDialog },
}}
/>
<Dialog open={openAddDialog} onClose={handleClose}>
<DialogTitle>Zutat hinzufügen</DialogTitle>
<DialogContent>
<DialogContentText>
Füge eine neue Zutat hinzu:
</DialogContentText>
<Grid container spacing={1}>
<Grid item xs={12}>
<TextField
fullWidth
margin="dense"
id="nameField"
//defaultValue={name != null ? name : ""}
value={newIngredient.name}
onChange={(e) => setNewIngredient({ ...newIngredient, name: e.target.value})}
//error={(article.name.length === 0)}
label="Name"
type="text"
variant="standard"
//onBlur={(e) => handleArticleChangeAPI(e,'name', e.target.value)}
/>
</Grid>
<Grid item xs={12}>
<ListFieldApiwoArticle
apiURL="/article/allergenes/"
titleName="Allergene"
addName="Zutat"
multiple={true}
value={newIngredient.allergenes}
onChange={(e) => setNewIngredient({ ...newIngredient, allergenes: e})}
addNew={false}
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel control={<Checkbox checked={newIngredient.vegan} onChange={(e) => setNewIngredient({ ...newIngredient, vegan: e.target.checked})} />} label="vegan?" />
<FormControlLabel control={<Checkbox checked={newIngredient.vegetarian} onChange={(e) => setNewIngredient({ ...newIngredient, vegetarian: e.target.checked})} />} label="vegetarisch?" />
<FormControlLabel control={<Checkbox checked={newIngredient.palmoilfree} onChange={(e) => setNewIngredient({ ...newIngredient, palmoilfree: e.target.checked})} />} label="Palmölfrei?" />
</Grid>
<Grid item xs={12}>
<FormControlLabel control={<Checkbox checked={newIngredient.additive} onChange={(e) => setNewIngredient({ ...newIngredient, additive: e.target.checked})} />} label="Zusatzstoff?" />
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
margin="dense"
id="EnumberField"
disabled={!newIngredient.additive}
value={newIngredient.eNumber}
onChange={(e) => setNewIngredient({ ...newIngredient, eNumber: e.target.value})}
error={(newIngredient.eNumber.length === 0 && newIngredient.additive)}
label="E-Nummer"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={12}>
<ListFieldApiwoArticle
apiURL="/article/ingrediences/"
titleName="Inhaltsstoffe"
addName="Zutat"
multiple={true}
value={newIngredient.subIngredients}
onChange={(e) => setNewIngredient({ ...newIngredient, subIngredients: e})}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddDialog(false)}>Abbrechen</Button>
<Button onClick={handleAdd}>Hinzufügen</Button>
</DialogActions>
</Dialog>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
);
}
export default IngredienceDataGrid

View File

@@ -0,0 +1,152 @@
import React from 'react'
import axios from 'axios';
import {useState, useEffect} from 'react'
import {TextField, Autocomplete, createFilterOptions} from '@mui/material'
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
const filter = createFilterOptions();
const baseURL = "http://localhost:8000/api"
const ListFieldApi = (props) => {
const { apiURL, titleName, addName, multiple, value, onChange, sx, article, fieldName } = props
const [list, setList] = useState([""]);
const [inputValue, setInputValue] = React.useState('')
const [snackbar, setSnackbar] = useState(null);
const handleAdd = async(newValue) => {
const send = await axios.post(baseURL+apiURL+'/', {
name: newValue,
})
const rec = await axios.get(baseURL+apiURL)
setList(rec.data);
};
const handleCloseSnackbar = () => setSnackbar(null);
const handleOnChange = async (event, newValue) => {
//Neuer Wert ist letztes Array element
//console.log(event)
//console.log(newValue)
if (newValue === null){
}
// Fall das wir mehr als einen Eintrag haben
else if (Array.isArray(newValue)) {
if (!(newValue.length === 0)){
// Wenn letztes array element nicht in der Liste der möglichen, dann haben wir ein neues Element was wir hinzufügen
if (!list.some(item => item.name === newValue[newValue.length - 1].name)) {
handleAdd(newValue[newValue.length - 1].inputValue);
newValue[newValue.length - 1].name = newValue[newValue.length - 1].inputValue;
delete newValue[newValue.length - 1]["inputValue"];
}
}
}
// Hinzufügen des ersten Elements
else{
if (!list.some(item => item.name === newValue.name)) {
handleAdd(newValue.inputValue);
newValue.name = newValue.inputValue;
delete newValue["inputValue"];
}
}
try {
const newArticle = { ...article, [fieldName]: newValue};
const sendArticle = await axios.put(baseURL+"/article/"+article.pk, {
article: newArticle
});
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
onChange(newArticle);
}
catch(error){
setSnackbar({ children: error.message, severity: 'error' });
if (Array.isArray(newValue)) {
newValue = newValue.slice(0, -1);
}
else {
newValue = null;
}
}
}
useEffect(()=>{
axios.get(baseURL+apiURL)
.then((res) => {
const data = res.data;
setList(data);
//console.log(data)
})
},[])
return (
<div>
<Autocomplete
multiple={multiple}
id="tags-standard"
options={list}
sx={sx}
getOptionLabel={(option) => {
if (option.name === undefined){
return ""
}
else{
return option.name
}
}}
isOptionEqualToValue={(option, value) => option.name === value.name}
inputValue={inputValue}
onInputChange={(_, newInputValue) => {
setInputValue(newInputValue)
}}
//defaultValue={[]}
value={value}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label={titleName}
placeholder={addName}
/>
)}
onChange={handleOnChange}
filterOptions={(options, params) => {
const filtered = filter(options, params);
if (params.inputValue !== '') {
filtered.push({
inputValue: params.inputValue,
name: `Add "${params.inputValue}"`,
});
}
return filtered;
}}
renderOption={(props, option) => <li {...props}>{option.name}</li>}
/>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
)
}
export default ListFieldApi

View File

@@ -0,0 +1,154 @@
import React from 'react'
import axios from 'axios';
import {useState, useEffect} from 'react'
import {TextField, Autocomplete, createFilterOptions} from '@mui/material'
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
const filter = createFilterOptions();
const baseURL = "http://localhost:8000/api"
const ListFieldApiwoArticle = (props) => {
const { apiURL, titleName, addName, multiple, value, onChange, sx, addNew} = props
const [list, setList] = useState([""]);
const [inputValue, setInputValue] = React.useState('')
const [snackbar, setSnackbar] = useState(null);
const handleAdd = async(newValue) => {
const send = await axios.post(baseURL+apiURL, {
name: newValue,
})
const rec = await axios.get(baseURL+apiURL)
setList(rec.data);
};
const handleCloseSnackbar = () => setSnackbar(null);
const handleOnChange = async (event, newValue) => {
//Neuer Wert ist letztes Array element
//console.log(event)
//console.log(newValue)
if (newValue === null){
}
// Fall das wir mehr als einen Eintrag haben
else if (Array.isArray(newValue)) {
if (!(newValue.length === 0)){
// Wenn letztes array element nicht in der Liste der möglichen, dann haben wir ein neues Element was wir hinzufügen
if (!list.some(item => item.name === newValue[newValue.length - 1].name)) {
handleAdd(newValue[newValue.length - 1].inputValue);
newValue[newValue.length - 1].name = newValue[newValue.length - 1].inputValue;
delete newValue[newValue.length - 1]["inputValue"];
}
}
}
// Hinzufügen des ersten Elements
else{
if (!list.some(item => item.name === newValue.name)) {
handleAdd(newValue.inputValue);
newValue.name = newValue.inputValue;
delete newValue["inputValue"];
}
}
/*
try {
const newArticle = { ...article, [fieldName]: newValue};
const sendArticle = await axios.put(baseURL+"/article/"+article.pk, {
article: newArticle
});
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
*/
onChange(newValue);
/*
}
catch(error){
setSnackbar({ children: error.message, severity: 'error' });
if (Array.isArray(newValue)) {
newValue = newValue.slice(0, -1);
}
else {
newValue = null;
}
}
*/
}
useEffect(()=>{
axios.get(baseURL+apiURL)
.then((res) => {
const data = res.data;
setList(data);
console.log(data)
})
},[])
return (
<div>
<Autocomplete
multiple={multiple}
id="tags-standard"
options={list}
sx={sx}
getOptionLabel={(option) => {
if (option.name === undefined){
return ""
}
else{
return option.name
}
}}
isOptionEqualToValue={(option, value) => option.name === value.name}
inputValue={inputValue}
onInputChange={(_, newInputValue) => {
setInputValue(newInputValue)
}}
//defaultValue={[]}
value={value}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label={titleName}
placeholder={addName}
/>
)}
onChange={handleOnChange}
filterOptions={(options, params) => {
const filtered = filter(options, params);
if (params.inputValue !== '' && addNew) {
filtered.push({
inputValue: params.inputValue,
name: `Add "${params.inputValue}"`,
});
}
return filtered;
}}
renderOption={(props, option) => <li {...props}>{option.name}</li>}
/>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
)
}
export default ListFieldApiwoArticle

View File

@@ -0,0 +1,271 @@
import {useState, useEffect} from 'react'
import React from 'react'
import { Grid, TextField } from '@mui/material'
import Button from '@mui/material/Button'
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import axios from 'axios';
const baseURL = "http://localhost:8000/api"
const MarketDetail = (props) => {
const [openExchangeDialog, setOpenExchangeDialog] = useState(false);
const [exchangeMarket, setExchangeMarket] = useState({oldMarket: {}, newMarket: {name: '', street: '', street_number: 0, zip_code: '', city: '', phone:''}, deleteOld: false});
const [markets, setMarkets] = useState([]);
const [snackbar, setSnackbar] = useState(null);
const [market, setMarket] = useState({
name: '',
street: '',
street_number: 0,
zip_code: '',
city: '',
phone: ''
});
useEffect(()=>{
axios.get(baseURL+"/market/"+props.marketId)
.then((res) => {
const data = res.data;
//console.log(data)
setMarket(data);
})
axios.get(baseURL+"/markets/")
.then((res) => {
console.log(res.data)
setMarkets(res.data);
})
},[])
const handleExchange = () => {
setExchangeMarket({ ...exchangeMarket, oldMarket: market });
setOpenExchangeDialog(true);
}
const handleExchangeClose = () => {
setOpenExchangeDialog(false);
}
const handleMarketExchange = async () => {
console.log(exchangeMarket)
try {
const receivedData = await axios.post(baseURL+"/exchangePurchaseMarket/"+props.purchaseId, {
data: exchangeMarket,
})
//const newData = tableData.map((row) => (row.id === exchangeArticle.oldRow.id ? receivedData.data : row));
setMarket(exchangeMarket.newMarket);
//const receivedData = null
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setOpenExchangeDialog(false);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
}
const handleCloseSnackbar = () => setSnackbar(null);
const renderExchangeDialog = () => {
return (
<Dialog open={openExchangeDialog} onClose={handleExchangeClose}>
<DialogTitle>Austausch eines Marketes gegen einen vorhandenen Markt</DialogTitle>
<DialogContent>
<Grid container spacing={1}>
<Grid item xs={5} alignItems="stretch" style={{ display: "flex" }}>
Markt austauschen gegen:
</Grid>
<Grid item xs={7} alignItems="stretch" style={{ display: "flex" }}>
<Select
fullWidth
id="exchangeArticle"
value={exchangeMarket.newMarket.name}
label="Market"
onChange={(e,t) => {setExchangeMarket({...exchangeMarket, newMarket: markets.filter((element) => element.id === Number(t.key.replace('.$','')))[0] });}}
>
{markets.map((market) => (
<MenuItem
key={market.id}
value={market.name}
//style={getStyles(name, personName, theme)}
>
{market.name+', '+market.street+' '+market.street_number+', '+market.city}
</MenuItem>
))}
</Select>
</Grid>
<FormControlLabel control={
<Checkbox
label='delete?'
checked={exchangeMarket.deleteOld}
onChange={(e) => {setExchangeMarket({ ...exchangeMarket, deleteOld: !exchangeMarket.deleteOld });}}
inputProps={{ 'aria-label': 'controlled' }}
/>
} label="Ursprünglichen Artikel löschen?"
/>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleExchangeClose}>Abbrechen</Button>
<Button onClick={handleMarketExchange}>Änderung speichern</Button>
</DialogActions>
</Dialog>
)
}
return(
<Grid container spacing={1}>
{renderExchangeDialog()}
<Grid item xs={12}>
<TextField
autoFocus
fullWidth
margin="dense"
id="name"
value={market.name}
/*
onChange={(event) =>
setMarket({
...market,
name: event.target.value,
})
}
*/
label="Name"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={9}>
<TextField
margin="dense"
fullWidth
id="street"
value={market.street}
/*
onChange={(event) =>
setMarket({
...market,
street: event.target.value,
})
}
*/
label="Straße"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={2}>
<TextField
margin="dense"
id="streetnum"
fullWidth
value={market.street_number}
/*
onChange={(event) =>
setMarket({
...market,
streetNum: event.target.value,
})
}
*/
label="Nr."
type="number"
variant="standard"
/>
</Grid>
<Grid item xs={4}>
<TextField
margin="dense"
id="zip"
fullWidth
value={market.zip_code}
/*
onChange={(event) =>
setMarket({
...market,
zip: event.target.value,
})
}
*/
label="PLZ"
type="number"
variant="standard"
/>
</Grid>
<Grid item xs={8}>
<TextField
margin="dense"
id="city"
fullWidth
value={market.city}
/*
onChange={(event) =>
setMarket({
...market,
city: event.target.value,
})
}
*/
label="Stadt"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={12}>
<TextField
margin="dense"
id="phone"
fullWidth
value={market.phone}
/*
onChange={(event) =>
setMarket({
...market,
tel: event.target.value,
})
}
*/
label="Telefon"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={3}>
<Button onClick={handleExchange}>Markt austauschen</Button>
</Grid>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</Grid>
)
}
export default MarketDetail

View File

@@ -0,0 +1,90 @@
import { AppBar, Toolbar, styled, Typography, IconButton, alpha, InputBase } from '@mui/material'
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import React from 'react'
const StyledToolbar = styled(Toolbar)({
display:"flex",
justifyContent:"space-between",
})
const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'inherit',
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
}));
const Navbar = () => {
return (
<AppBar
position="sticky"
>
<StyledToolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="open drawer"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
sx={{ display: {xs: "none", sm: "block"} }}
>
Kassenzettel
</Typography>
<Search>
<SearchIconWrapper>
<SearchIcon />
</SearchIconWrapper>
<StyledInputBase
placeholder="Search…"
inputProps={{ 'aria-label': 'search' }}
/>
</Search>
</StyledToolbar>
</AppBar>
)
}
export default Navbar

View File

@@ -0,0 +1,231 @@
import {useState, useEffect} from 'react'
import React from 'react'
import axios from 'axios';
import PropTypes from 'prop-types';
//import { useNavigate, useLocation } from 'react-router-dom';
import { Button } from '@mui/material'
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import { DataGrid, GridToolbarContainer, GridActionsCellItem } from '@mui/x-data-grid';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
const baseURL = "http://localhost:8000/api"
//const apiURL = "/article/packaging"
/*
- Crashes if deleting an entry
*/
function EditToolbar(props) {
const { setTableData, article } = props;
const handleClick = async () => {
const newValue = {id: Math.floor(Math.random() * 1000)+10, value: 0, isEstimated: false, unit: 'g'}
setTableData((oldRows) => [...oldRows, newValue]);
};
return (
<GridToolbarContainer>
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
Neuer Eintrag
</Button>
</GridToolbarContainer>
);
}
EditToolbar.propTypes = {
setTableData: PropTypes.func.isRequired,
}
const NutriDataGrid = (props) => {
const { article } = props;
const [tableData, setTableData] = useState([{id: -1, type: '', serving: 0, unit: 'g'}]);
const [options, setOptions] = useState([{id: -1, name: ''}]);
const [snackbar, setSnackbar] = useState(null);
const units = ['kg','g','mg','µg','l','ml','St','kJ','kcal'];
const columns = [
{
field: 'isEstimated',
headerName: 'Schätzung?',
width: 100,
editable: true,
type: 'boolean',
},
{
field: 'nutrient',
headerName: 'Nährwert',
width:150,
type: 'singleSelect',
//valueOptions: () => {return options.map(x => x.name)},
valueOptions: options,
getOptionValue: (value) => value.id,
getOptionLabel: (value) => value.name,
editable: true,
flex: 1,
valueGetter: (params) => {try {return params.row.nutrient.id;} catch {return '';} },
},
{
field: 'value',
headerName: '100g/100ml',
editable: true,
type: 'number',
//valueGetter: (params) => { return params.row.data.serving },
},
{
field: 'unit',
headerName: 'Einheit',
type: 'singleSelect',
valueOptions: units,
editable: true,
//valueGetter: (params) => { return params.row.data.unit },
},
/*
{field: 'portion', headerName: 'Portion'},
*/
{
field: 'actions',
type: 'actions',
headerName: 'Aktionen',
width: 100,
cellClassName: 'actions',
getActions: ({ id }) => {
return [
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={(e) => handleDeleteClick(id,e)}
color="inherit"
/>,
];
},
}
]
const handleCloseSnackbar = () => setSnackbar(null);
//Anscheinend ist es notwendig das event e zu übergeben, ansonsten passieren komische Dinge (kein async, multiple trigger beim rendern etc)
const handleDeleteClick = async (id,e) => {
const newData = tableData.filter((row) => row.id !== id);
try {
const receivedData = await axios.delete(baseURL+"/article/nutritionalValues/"+article.pk, {
data: id,
})
setSnackbar({ children: 'Nährwert erfolgreich gelöscht', severity: 'success' });
setTableData(newData);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
};
const processRowUpdate = React.useCallback( async (newRow, oldRow) => {
console.log(newRow)
var nutrient;
if (newRow.nutrient === undefined) {
return newRow;
}
else if (typeof newRow.nutrient === 'number') {
nutrient = options.find(x => x.id === newRow.nutrient);
}
else {
nutrient = newRow.nutrient;
}
const updatedRow = {id: newRow.id, article_id: article.pk, nutrient: nutrient, value: newRow.value, unit: newRow.unit, isEstimated: newRow.isEstimated, isNew: false};
const newData = tableData.map((row) => (row.id === newRow.id ? updatedRow : row));
//console.log(newData)
try {
//const newCompleteData = {article: article.pk, data: newData, id: id};
//console.log(newCompleteData)
const response = await axios.put(baseURL+"/article/nutritionalValues/"+article.pk, {
nutriData: updatedRow
});
//console.log(response);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setTableData(newData);
//onChange(newData);
return updatedRow;
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
return oldRow;
}
}
);
//const handleProcessRowUpdateError = React.useCallback((error) => {
// setSnackbar({ children: error.message, severity: 'error' });
//}, []);
useEffect(() =>{
async function fetchOptions() {
const ans = await axios.get(baseURL+"/article/nutrients/");
setOptions(ans.data);
}
async function fetchData() {
const values = await axios.get(baseURL+"/article/nutritionalValues/"+article.pk);
setTableData(values.data);
}
fetchOptions();
fetchData();
},[article])
return (
<div style={{ height: 570, width: '100%' }}>
<DataGrid
rows={tableData}
columns={columns}
pageSize={10}
editMode="cell"
//getRowId={(row) => row.internalId}
processRowUpdate={processRowUpdate}
//onProcessRowUpdateError={handleProcessRowUpdateError}
//isCellEditable={(params) => {console.log(params);return !permanentOptions.includes(params.field) }}
experimentalFeatures={{ newEditingApi: true }}
components={{
Toolbar: EditToolbar,
}}
componentsProps={{
toolbar: { setTableData, article },
}}
initialState={{
sorting: {
sortModel: [{ field: 'id', sort: 'desc' }],
},
}}
/>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
);
}
export default NutriDataGrid

View File

@@ -0,0 +1,240 @@
import {useState, useEffect} from 'react'
import React from 'react'
import axios from 'axios';
import PropTypes from 'prop-types';
//import { useNavigate, useLocation } from 'react-router-dom';
import { Button, Autocomplete, TextField } from '@mui/material'
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Close';
import { DataGrid, GridRowModes, GridToolbarContainer, GridActionsCellItem } from '@mui/x-data-grid';
import ListFieldApi from "./ListFieldApi.component"
const baseURL = "http://localhost:8000/api"
const apiURL = "/article/packaging"
const makeId = () => {
let ID = "";
let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for ( var i = 0; i < 12; i++ ) {
ID += characters.charAt(Math.floor(Math.random() * 36));
}
return ID;
}
const EditToolbar = (props) => {
const { onChange, setRowModesModel, setList } = props;
const [inputValue, setInputValue] = React.useState('')
const handleClick = () => {
const id = makeId();
onChange([{ id, packaging: '', weight: '', isNew: true }]);
setRowModesModel((oldModel) => ({
...oldModel,
[id]: { mode: GridRowModes.Edit, fieldToFocus: 'packaging' },
}));
};
const handelMaterialChange = (event) => {
if (event == null){
setInputValue('')
}
else{
setInputValue(event.name);
}
//axios.get(baseURL+apiURL)
// .then((res) => {
// const data = res.data;
// setList(data);
// //console.log(data)
// })
}
return (
<GridToolbarContainer>
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
Eintrag hinzufügen
</Button>
<ListFieldApi
apiURL={apiURL}
titleName="Material hinzufügen"
addName="Material"
multiple={false}
value={inputValue}
onChange={handelMaterialChange}
sx={{ width: 300 }}
/>
</GridToolbarContainer>
);
}
EditToolbar.propTypes = {
setRowModesModel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
const initialRows = [
{
id: makeId(),
packaging: 'Pappe',
weight: 0,
},
]
const PackagingDataGrid = (props) => {
const { value, onChange, setArticle,...rest } = props;
const [rows, setRows] = React.useState([]);
const [rowModesModel, setRowModesModel] = React.useState({});
const [list, setList] = useState([]);
const [inputValue, setInputValue] = React.useState('')
const columnsPackaging = [
{
field: 'packaging',
headerName: 'Material',
width: 350,
type: 'singleSelect',
valueOptions: () => {return list.map(x => x.name)},
editable: true,
},
{
field: 'weight',
headerName: 'Gewicht in g',
type: 'number',
width:100,
editable: true,
},
{
field: 'actions',
type: 'actions',
headerName: 'Actions',
width: 150,
cellClassName: 'actions',
getActions: ({ id }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
onClick={handleSaveClick(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
className="textPrimary"
onClick={handleCancelClick(id)}
color="inherit"
/>,
];
}
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className="textPrimary"
onClick={handleEditClick(id,rows)}
color="inherit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={handleDeleteClick(id)}
color="inherit"
/>,
];
},
},
]
//const handleRowEditStart = (params, event) => {
// event.defaultMuiPrevented = true;
//};
const handleRowEditStop = (params, event) => {
event.defaultMuiPrevented = true;
};
const handleEditClick = (id, rows) => () => {
console.log(rowModesModel)
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
};
const handleSaveClick = (id) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
};
const handleDeleteClick = (id) => () => {
setRows(rows.filter((row) => row.id !== id));
};
const handleCancelClick = (id) => () => {
setRowModesModel({
...rowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true },
});
const editedRow = rows.find((row) => row.id === id);
if (editedRow.isNew) {
setRows(rows.filter((row) => row.id !== id));
}
};
const processRowUpdate = (newRow) => {
const updatedRow = { ...newRow, isNew: false };
console.log(newRow)
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
return updatedRow;
};
useEffect(()=>{
axios.get(baseURL+apiURL)
.then((res) => {
const data = res.data;
setList(data);
//console.log(data)
})
},[])
return (
<DataGrid
rows={value}
columns={columnsPackaging}
pageSize={10}
editMode="row"
rowModesModel={rowModesModel}
//onRowEditStart={handleRowEditStart}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
components={{
Toolbar: EditToolbar,
}}
componentsProps={{
toolbar: { onChange, setRowModesModel, setList },
}}
experimentalFeatures={{ newEditingApi: true }}
//onRowEditStop={handleRowEditStop}
//checkboxSelection
//disableSelectionOnClick
//onCellClick={handleDataGridClick}
//onCellDoubleClick={handleDataGridDoubleClick}
/>
);
}
export default PackagingDataGrid

View File

@@ -0,0 +1,207 @@
import {useState, useEffect} from 'react'
import React from 'react'
import axios from 'axios';
import PropTypes from 'prop-types';
//import { useNavigate, useLocation } from 'react-router-dom';
import { Button } from '@mui/material'
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import { DataGrid, GridToolbarContainer, GridActionsCellItem } from '@mui/x-data-grid';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
const baseURL = "http://localhost:8000/api"
const apiURL = "/article/packaging/"
function EditToolbar(props) {
const { setTableData, articleId } = props;
const handleClick = async () => {
const newValue = { packaging_id: {id: -1, name: 'Bitte auswählen'}, weight: 0, isNew: true, article_id: articleId };
const receivedData = await axios.post(baseURL+"/article/articlePackagings/"+articleId, {
newValue,
})
//console.log(receivedData)
const newRow = { id: receivedData.data.id, packaging_id: {id: -1, name: 'Bitte auswählen'}, weight: receivedData.data.weight, isNew: true, article_id: receivedData.data.article_id };
setTableData((oldRows) => [...oldRows, newRow]);
};
return (
<GridToolbarContainer>
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
Neuer Eintrag
</Button>
</GridToolbarContainer>
);
}
EditToolbar.propTypes = {
setTableData: PropTypes.func.isRequired,
}
const PackagingDataGrid2 = (props) => {
const { articleId } = props;
const [list, setList] = useState([]);
const [tableData, setTableData] = useState([{id: -1, packaging_id: {id: -1, name: ''}, weight: -1, article_id: -1}]);
const [snackbar, setSnackbar] = useState(null);
const columnsPackaging = [
{
field: 'packaging_id',
headerName: 'Material',
flex: 1,
type: 'singleSelect',
valueOptions: () => {return list.map(x => x.name).sort()},
editable: true,
valueGetter: (params) => {try {return params.row.packaging_id.name;} catch {return '';} },
},
{
field: 'weight',
headerName: 'Gewicht in g',
type: 'number',
width:100,
editable: true,
},
{
field: 'actions',
type: 'actions',
headerName: 'Aktionen',
width: 100,
cellClassName: 'actions',
getActions: ({ id }) => {
return [
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={(e) => handleDeleteClick(id,e)}
color="inherit"
/>,
];
},
}
]
const findId = (idToLookFor) => {
for (var i = 0; i < list.length; i++) {
if (list[i].name === idToLookFor) {
return(list[i].id);
}
}
}
const handleCloseSnackbar = () => setSnackbar(null);
//Anscheinend ist es notwendig das event e zu übergeben, ansonsten passieren komische Dinge (kein async, multiple trigger beim rendern etc)
const handleDeleteClick = async (id,e) => {
const newData = tableData.filter((row) => row.id !== id);
//console.log('Delete clicked')
//console.log(id)
//console.log(e)
try {
const receivedData = await axios.delete(baseURL+"/article/articlePackagings/"+articleId, {
data: id,
})
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setTableData(newData);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
};
const processRowUpdate = React.useCallback( async (newRow, oldRow) => {
const id = findId(newRow.packaging_id);
const updatedRow = {'id': newRow.id, 'packaging_id':{'id': id, 'name': newRow.packaging_id}, 'weight': newRow.weight, 'isNew': false, 'article_id': articleId};
const newData = tableData.map((row) => (row.id === newRow.id ? updatedRow : row));
try {
const response = await axios.put(baseURL+"/article/articlePackagings/"+articleId, {
updatedRow
});
//console.log(response);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setTableData(newData);
//onChange(newData);
return updatedRow;
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
return oldRow;
}
}
);
//const handleProcessRowUpdateError = React.useCallback((error) => {
// setSnackbar({ children: error.message, severity: 'error' });
//}, []);
useEffect(()=>{
axios.get(baseURL+apiURL)
.then((res) => {
const data = res.data;
setList(data);
})
axios.get(baseURL+"/article/articlePackagings/"+articleId)
.then((res) => {
const data = res.data;
//Add id for each element for displaying in Data Grid
//data.forEach((item, i) => {
// item.id = i + 1;
//});
setTableData(data);
//console.log("Fetching packaging data")
//console.log(data)
})
},[])
return (
<div style={{ height: 350, width: '100%' }}>
<DataGrid
rows={tableData}
columns={columnsPackaging}
pageSize={10}
editMode="row"
//getRowId={(row) => row.internalId}
processRowUpdate={processRowUpdate}
//onProcessRowUpdateError={handleProcessRowUpdateError}
experimentalFeatures={{ newEditingApi: true }}
components={{
Toolbar: EditToolbar,
}}
componentsProps={{
toolbar: { setTableData, articleId },
}}
/>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
);
}
export default PackagingDataGrid2

View File

@@ -0,0 +1,999 @@
import {useState, useEffect} from 'react'
import React from 'react'
import axios from 'axios';
import dayjs from 'dayjs';
import { useNavigate, useLocation } from 'react-router-dom';
import { DataGrid, GridToolbarContainer, GridActionsCellItem, GridRowModes } from '@mui/x-data-grid';
import PropTypes from 'prop-types';
import Grid from '@mui/material/Grid'
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import Checkbox from '@mui/material/Checkbox';
import InputAdornment from '@mui/material/InputAdornment';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import {Autocomplete, createFilterOptions} from '@mui/material'
import { grey } from '@mui/material/colors';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import MarketDetail from "./MarketDetail.component";
import ReceipeCanvas from "./Canvas.component"
import ListFieldApiwoArticle from "./ListFieldApiwoArticle.component"
const baseURL = "http://localhost:8000/api"
const filter = createFilterOptions();
function EditToolbar(props) {
const { setArticles, setOpenNewEntryDialog } = props;
const handleClick = async () => {
console.log("Neuer Eintrag")
setOpenNewEntryDialog(true);
const retArticles = await axios.get(baseURL+"/articles/", {
});
// Here we just add a "Neuer Artikel" object to the list of all available articles. If this is selected later on, we ask the backend for a real new object.
const answerWithNewArticelOption = retArticles.data.concat({pk:-1, name: 'Neuer Artikel', brand: null});
setArticles(answerWithNewArticelOption);
};
return (
<GridToolbarContainer>
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
Neuer Eintrag
</Button>
</GridToolbarContainer>
);
}
EditToolbar.propTypes = {
setArticles: PropTypes.func.isRequired,
}
const PurchaseDetails = (props) => {
const location = useLocation();
//location.state != null ? console.log(location.state.id) : console.log("No data to show");
const navigate = useNavigate();
const noButtonRef = React.useRef(null);
const drawRef = React.useRef({});
const [img, setImg] = useState(new Image());
const [tableData, setTableData] = useState([{id: 1, quantity: 0, article_id: {name: '', brand: {name: ''}}, price: 0, MwSt: 7, inSale: false, map: '', purchase_id: -1}]);
const [drawData, setDrawData] = useState([]);
const [snackbar, setSnackbar] = useState(null);
const [rowModesModel, setRowModesModel] = useState({});
const [purchaseArticleRowId, setPurchaseArticleRowId] = useState(null);
const [doubleClickAllowed, setDoubleClickAllowed] = useState(true);
const [promiseArguments, setPromiseArguments] = React.useState(null);
const [openMapDialog, setOpenMapDialog] = useState(false);
const [openExchangeDialog, setOpenExchangeDialog] = useState(false);
const [openNewEntryDialog, setOpenNewEntryDialog] = useState(false);
const [mapString, setMapString] = useState({receipeString: {id: -1, name: ""}, location_x: 0, location_y: 0, location_h: 0, location_w: 0, pk: -1, rowId: -1, purchase_id: -1, receipeString: ""});
const [rowMode, setRowMod] = useState('view');
const [stringData, setStringData] = useState([]);
const [paymentType, setPaymentType] = useState(location.state.payment_type);
const [totalPrice, setTotalPrice] = useState(location.state.total_price);
const [purchaseDate, setpurchaseDate] = useState(location.state.purchase_date);
const [newArticle,setNewArticle] = useState({id: 1, quantity: 0, article_id: {name: '', brand: {name: ''}}, price: 0, MwSt: 7, inSale: false, map: '', purchase_id: -1});
const [articles, setArticles] = useState([]);
const [exchangeArticle, setExchangeArticle] = useState({rowData: {}, newArticle: {}, deleteOld: false});
//const [purchaseArticleRowId, setPurchaseArticleRowId, clearPurchaseArticleRowId] = useStickyState()
const paymentTypes = ['','EC','Bar','Kreditkarte']
const columns = [
{
field: 'quantity',
headerName: '#',
width: 60,
type: 'number',
editable: true,
},
{
field: 'name',
headerName: 'Artikel',
valueGetter: (params) => { const brandName = params.row.article_id.brand === null ? '' : params.row.article_id.brand.name; const articleName = params.row.article_id === null ? '' : params.row.article_id.name;
return brandName + ' ' + articleName },
width: 190,
type: 'string',
},
{
field: 'price',
headerName: 'Preis',
width: 80,
type: 'number',
editable: true,
valueFormatter: (params) => {
if (params.value == null) {
return '';
}
//const valueFormatted = Number(params.value * 100).toLocaleString();
return `${params.value}`;
},
},
{
field: 'MwSt',
headerName: 'MwSt',
valueGetter: (params) => { return params.row.article_id.MwSt },
valueFormatter: (params) => {
if (params.value == null) {
return '';
}
//const valueFormatted = Number(params.value * 100).toLocaleString();
return `${params.value} %`;
},
width: 55,
type: 'number',
editable: true,
},
{
field: 'inSale',
headerName: 'Sale',
type: 'boolean',
width: 55,
editable: true,
},
{
field: 'sum_price',
headerName: '∑ Preise',
width: 80,
type: 'number',
valueFormatter: (params) => {
if (params.value == null) {
return '';
}
//const valueFormatted = Number(params.value * 100).toLocaleString();
return `${params.value.toFixed(2)}`;
},
},
{
field: 'actions',
type: 'actions',
headerName: 'Aktionen',
width: 100,
cellClassName: 'actions',
getActions: ({ id }) => {
return [
<GridActionsCellItem
icon={(rowMode === 'edit' && purchaseArticleRowId === id) ? <SaveIcon /> : <EditIcon />}
label="Edit"
onClick={(e) => handleEditSaveClick(e,id)}
color="inherit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={(e) => handleDeleteClick(id,e)}
color="inherit"
/>,
<GridActionsCellItem
icon={<EditIcon />}
label="Kassenzettel-String bearbeiten"
onClick={(e) => handleEditMapClick(id,e)}
showInMenu
/>,
<GridActionsCellItem
icon={<EditIcon />}
label="Artikel austauschen"
onClick={(e) => handleExchangeClick(id,e)}
showInMenu
/>,
<GridActionsCellItem
icon={<EditIcon />}
label="Bereich übernehmen"
onClick={(e) => handleSaveLocation(id,e)}
showInMenu
/>,
];
},
}
]
const handleCloseSnackbar = () => setSnackbar(null);
const handleDataGridClick = (e) => {
if (purchaseArticleRowId !== e.row.id) {
setRowModesModel({ ...rowModesModel, [purchaseArticleRowId]: { mode: GridRowModes.View } });
setDoubleClickAllowed(true);
setRowMod('view')
}
setPurchaseArticleRowId( e.row.id );
//setDrawData(e.row.map)
drawRef.current.draw(e.row.map)
}
const handleDataGridDoubleClick = (e) => {
console.log("PurchaseDetail->DoubleClick")
console.log(e.row)
if (doubleClickAllowed) {
navigate('/ArticleDetails', {state:e.row});
}
}
const handleEditSaveClick = (e, id) => {
if (!purchaseArticleRowId) {
return;
}
if (rowMode === 'edit') {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
setDoubleClickAllowed(true);
setRowMod('view')
} else {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
setDoubleClickAllowed(false);
setRowMod('edit')
}
//console.log("Edit")
};
const processRowUpdate = React.useCallback( async (newRow, oldRow) => {
const newArticle = { ...newRow.article_id, MwSt: newRow.MwSt };
const updatedRow = {id: newRow.id, MSt: newRow.MwSt, article_id: newArticle, inSale: newRow.inSale,
map: newRow.map, net_weight: newRow.net_weight, price: newRow.price,
quantity: newRow.quantity, sum_price: newRow.quantity*newRow.price
};
const newData = tableData.map((row) => (row.id === updatedRow.id ? updatedRow : row));
try {
const response = await axios.put(baseURL+"/purchase/article/"+newRow.id, {
newRow: updatedRow
});
//console.log(response);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setTableData(newData);
updateTotalPrice(newData);
//onChange(newData);
return updatedRow;
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
return oldRow;
}
});
const handleDeleteClick = async (id,e) => {
console.log("Delete: "+id)
/*
try {
const receivedData = await axios.delete(baseURL+"/purchase/article/"+id, {
data: id,
})
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setTableData(newData);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}*/
new Promise((resolve, reject) => {
setPromiseArguments({ resolve, reject, id });
resolve(); // Nothing was changed
})
};
const handleEditMapClick = (id, e) => {
const currentRow = tableData.filter((row) => row.id === id)[0];
setMapString({...currentRow.map,rowId: id});
setOpenMapDialog(true);
}
const handleSaveLocation = async (id, e) => {
var currentRow = tableData.filter((row) => row.id === id)[0];
const locationData = {pk: currentRow.map.pk,
receipeString: currentRow.map.receipeString,
location_x: drawData.location_x,
location_y: drawData.location_y,
location_h: drawData.location_h,
location_w: drawData.location_w
};
try {
const receivedData = await axios.put(baseURL+"/article/maps/"+locationData.pk, {
data: locationData,
})
currentRow.map = locationData;
const newData = tableData.map((row) => (row.id === currentRow.id ? currentRow : row));
setTableData(newData);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
}
const handleMapClose = () => {
setOpenMapDialog(false);
}
const handleExchangeClick = (id, e) => {
const currentRow = tableData.filter((row) => row.id === id)[0];
console.log(currentRow);
setExchangeArticle({ ...exchangeArticle, oldRow: currentRow });
setOpenExchangeDialog(true);
}
const handleExchangeClose = () => {
setOpenExchangeDialog(false);
}
const handleNewEntryClose = async () => {
setOpenNewEntryDialog(false);
}
const handleMapEdit = async () => {
try {
//const newMap = { ...mapString, receipeString: mapString };
console.log(mapString)
const receivedData = await axios.put(baseURL+"/article/maps/"+mapString.pk, {
data: mapString,
})
//const receivedData = null
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
var newRow = tableData.filter((row) => row.id === mapString.rowId)[0];
newRow.map = mapString;
const newData = tableData.map((row) => (row.id === mapString.rowId ? newRow : row));
console.log(newData)
setTableData(newData);
setOpenMapDialog(false);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
}
const handleArticleExchange = async () => {
console.log(exchangeArticle)
try {
//const newMap = { ...mapString, receipeString: mapString };
console.log(mapString)
const receivedData = await axios.post(baseURL+"/exchangePurchaseArticles/", {
data: exchangeArticle,
})
const newData = tableData.map((row) => (row.id === exchangeArticle.oldRow.id ? receivedData.data : row));
setTableData(newData);
//const receivedData = null
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setOpenExchangeDialog(false);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
}
const handleNewEntryAdd = async () => {
console.log(newArticle);
if (newArticle.article_id.name === "Neuer Artikel") {
try {
const receivedData = await axios.post(baseURL+"/newPurchaseArticles/"+location.state.id+"/", {
data: newArticle,
})
console.log("Received new article:")
console.log(receivedData.data)
const newTableData = tableData.concat(receivedData.data);
console.log(newTableData)
setTableData(newTableData);
updateTotalPrice(newTableData);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setOpenNewEntryDialog(false);
setNewArticle({id: 1, quantity: 0, article_id: {name: '', brand: {name: ''}}, price: 0, MwSt: 7, inSale: false, map: '', purchase_id: -1})
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
} else {
try {
const sendData = {...newArticle, article_id: newArticle.article_id}
const receivedData = await axios.post(baseURL+"/purchaseArticles/"+location.state.id+"/", {
data: sendData,
})
//console.log(receivedData.data)
const newTableData = tableData.concat(receivedData.data);
console.log(receivedData.data)
setTableData(newTableData);
updateTotalPrice(newTableData);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setOpenNewEntryDialog(false);
setNewArticle({id: 1, quantity: 0, article_id: {name: '', brand: {name: ''}}, price: 0, MwSt: 7, inSale: false, map: '', purchase_id: -1})
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
}
}
}
const handleNewArticleChange = (value) => {
const selectedArticle = articles.filter((article) => article.pk === value.id )[0]
setNewArticle({ ...newArticle, article_id: selectedArticle })
}
const handleSavePrice = async () => {
console.log(location.state)
const newPurchaseData = { ...location.state, total_price: totalPrice, payment_type: paymentType, purchase_date: purchaseDate, edit_finished: true, receipeImage: location.state.receipeImage.pk};
try {
const receivedData = await axios.put(baseURL+"/purchase/"+newPurchaseData.id, {
data: newPurchaseData,
})
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setPromiseArguments(null);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
setPromiseArguments(null);
}
};
const handleSaveDate = async (val) => {
console.log(val.format("YYYY-MM-DD"))
const newPurchaseData = { ...location.state, purchase_date: val.format("YYYY-MM-DD"), receipeImage: location.state.receipeImage.pk};
try {
const receivedData = await axios.put(baseURL+"/purchase/"+newPurchaseData.id, {
data: newPurchaseData,
})
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
setPromiseArguments(null);
setpurchaseDate(val.format("YYYY-MM-DD"));
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
setPromiseArguments(null);
}
};
const handleNo = () => {
const { resolve } = promiseArguments;
resolve(); // Resolve with the old row to not update the internal state
setPromiseArguments(null);
};
// If in delete dialog box yes is choosen, we will delete the article in the backend
const handleYes = async () => {
const { reject, resolve, id } = promiseArguments;
try {
const receivedData = await axios.delete(baseURL+"/purchase/article/"+id, {
data: id,
})
const newData = tableData.filter((row) => row.id !== id);
setTableData(newData);
updateTotalPrice(newData);
setSnackbar({ children: 'Änderungen erfolgreich gespeichert', severity: 'success' });
resolve(receivedData);
setPromiseArguments(null);
} catch (error) {
setSnackbar({ children: error.message, severity: 'error' });
reject();
setPromiseArguments(null);
}
};
const handleEntered = () => {
// The `autoFocus` is not used because, if used, the same Enter that saves
// the cell triggers "No". Instead, we manually focus the "No" button once
// the dialog is fully open.
// noButtonRef.current?.focus();
};
const renderExchangeDialog = () => {
return (
<Dialog open={openExchangeDialog} onClose={handleExchangeClose}>
<DialogTitle>Austausch eines Artikels gegen einen vorhandenen Artikel</DialogTitle>
<DialogContent>
<Grid container spacing={1}>
<Grid item xs={5} alignItems="stretch" style={{ display: "flex" }}>
Artikel austauschen gegen:
</Grid>
<Grid item xs={7} alignItems="stretch" style={{ display: "flex" }}>
{/*
<Select
fullWidth
id="exchangeArticle"
value={exchangeArticle.newArticle.name}
label="Artikel"
onChange={(e,t) => {setExchangeArticle({ ...exchangeArticle, newArticle: articles.filter((element) => element.pk === Number(t.key.replace('.$','')))[0] });}}
>
{articles.map((article) => (
<MenuItem
key={article.pk}
value={article.name}
//style={getStyles(name, personName, theme)}
>
{(article.brand !== null ? article.brand.name : '')+' '+article.name}
</MenuItem>
))}
</Select> */}
<Autocomplete
multiple={false}
id="exchangeArticle"
fullWidth
options={articles.map((article) => (
{name: article.name, brand: article.brand !== null ? article.brand.name : '', id: article.pk}
)).sort((a, b) => {
const nameA = a.brand + ' ' +a.name.toUpperCase(); // ignore upper and lowercase
const nameB = b.brand + ' ' +b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
})
}
groupBy={(option) => option.brand}
renderOption={(props, option) => (
<Box component="li" {...props} key={option.id}>
{option.brand + ' ' +option.name}
</Box>
)}
getOptionLabel={(option) => {
if (option.name === undefined){
return ""
}
else{
return option.brand + ' ' +option.name
}
}}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label='Marke + Artikel'
/>
)}
//onChange={(event, newValue) => {handleNewArticleChange(newValue)}}
onChange={(e,t) => {setExchangeArticle({ ...exchangeArticle, newArticle: articles.filter((element) => element.pk === t.id)[0] });}}
/>
</Grid>
<FormControlLabel control={
<Checkbox
label='Sale?'
checked={exchangeArticle.deleteOld}
onChange={(e) => {setExchangeArticle({ ...exchangeArticle, deleteOld: !exchangeArticle.deleteOld });}}
inputProps={{ 'aria-label': 'controlled' }}
/>
} label="Ursprünglichen Artikel löschen?"
/>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleExchangeClose}>Abbrechen</Button>
<Button onClick={handleArticleExchange}>Änderung speichern</Button>
</DialogActions>
</Dialog>
)
}
const renderConfirmDialog = () => {
if (!promiseArguments) {
return null;
}
//const { newRow, oldRow } = promiseArguments;
return (
<Dialog
maxWidth="xs"
TransitionProps={{ onEntered: handleEntered }}
open={!!promiseArguments}
>
<DialogTitle>Sind sie sicher?</DialogTitle>
<DialogContent dividers>
{`Ein klick auf 'Ja' wir die Daten löschen.`}
</DialogContent>
<DialogActions>
<Button ref={noButtonRef} onClick={handleNo}>
Nein
</Button>
<Button onClick={handleYes}>Ja</Button>
</DialogActions>
</Dialog>
);
};
const updateTotalPrice = (val) => {
const sumWithInitial = val.reduce(
(accumulator, currentValue) => accumulator + currentValue.sum_price,
0
);
setTotalPrice(sumWithInitial.toFixed(2));
};
useEffect(()=>{
var myImage = new Image();
myImage.src = baseURL+"/receipeImage/"+location.state.receipeImage.pk;
setImg(myImage)
drawRef.current.drawImage(myImage);
axios.get(baseURL+"/purchaseArticles/"+location.state.id)
.then((res) => {
const data = res.data;
//console.log(tableData)
console.log(data)
setTableData(data);
updateTotalPrice(data);
})
axios.get(baseURL+"/articles/")
.then((res) => {
setArticles(res.data);
})
axios.get(baseURL+"/article/mapStrings/")
.then((res) => {
setStringData(res.data);
})
},[])
return(
<div style={{ height: 800, width: '100%' }}>
<h1> Details zum Einkauf vom {purchaseDate} </h1>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
value={dayjs(purchaseDate)}
onChange={handleSaveDate}
/>
</LocalizationProvider>
<Box sx={{ borderRadius: '4px', p: 2, border: '1px solid', borderColor: grey[300] }}>
<MarketDetail
marketId={location.state.market.id}
purchaseId={location.state.id}
/>
</Box>
<Grid container spacing={1}>
<Grid item xs={8}>
<div style={{ display: 'flex', height: '1200px' }}>
<div style={{ flexGrow: 1 }}>
{renderConfirmDialog()}
{renderExchangeDialog()}
{/*Map String Dialog*/}
<Dialog open={openMapDialog} onClose={handleMapClose}>
<DialogTitle>Bearbeiten des OCR-Artikel Strings</DialogTitle>
<DialogContent>
<Grid container spacing={1}>
{/*
<TextField
fullWidth
margin="dense"
id="mapStringField"
//defaultValue={name != null ? name : ""}
value={mapString.receipeString.receipeString}
onChange={(e) => setMapString({ ...mapString, receipeString: {...mapString.receipeString, receipeString: e.target.value} })}
error={(mapString.length === 0)}
label="OCR Mapping String"
type="text"
variant="standard"
/>
*/}
<ListFieldApiwoArticle
apiURL="/article/mapStrings/"
titleName="String"
addName="String hinzufügen"
multiple={false}
value={mapString.receipeString}
onChange={(e) => {setMapString({ ...mapString, receipeString: e })}}
sx={{width:'400px'}}
addNew={true}
/>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleMapClose}>Abbrechen</Button>
<Button onClick={handleMapEdit}>Änderung speichern</Button>
</DialogActions>
</Dialog>
<Dialog open={openNewEntryDialog} onClose={handleNewEntryClose}>
<DialogTitle>Neuer Eintrag hinzufügen</DialogTitle>
<DialogContent>
<Grid container spacing={1}>
<Grid item xs={1}>
<TextField
fullWidth
margin="dense"
id="newQuantityField"
//defaultValue={name != null ? name : ""}
value={newArticle.quantity}
onChange={(e) => setNewArticle({ ...newArticle, quantity: e.target.value })}
error={(newArticle.quantity === 0)}
label="#"
type="number"
variant="standard"
/>
</Grid>
<Grid item xs={5}>
<Autocomplete
multiple={false}
id="tags-standard"
fullWidth
options={articles.map((article) => (
{name: article.name, brand: article.brand !== null ? article.brand.name : '', id: article.pk}
)).sort((a, b) => {
const nameA = a.brand + ' ' +a.name.toUpperCase(); // ignore upper and lowercase
const nameB = b.brand + ' ' +b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
})
}
groupBy={(option) => option.brand}
renderOption={(props, option) => (
<Box component="li" {...props} key={option.id}>
{option.brand + ' ' +option.name}
</Box>
)}
getOptionLabel={(option) => {
if (option.name === undefined){
return ""
}
else{
return option.brand + ' ' +option.name
}
}}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label='Marke + Artikel'
/>
)}
onChange={(event, newValue) => {handleNewArticleChange(newValue)}}
/>
</Grid>
<Grid item xs={2}>
<TextField
fullWidth
margin="dense"
id="newPriceField"
//defaultValue={name != null ? name : ""}
value={newArticle.price}
onChange={(e) => setNewArticle({ ...newArticle, price: e.target.value })}
error={(newArticle.price === 0)}
label="Preis"
type="number"
variant="standard"
InputProps={{
endAdornment: <InputAdornment position="end"></InputAdornment>
}}
/>
</Grid>
<Grid item xs={2}>
<TextField
width='10px'
margin="dense"
id="newMwSTField"
//defaultValue={name != null ? name : ""}
value={newArticle.MwSt}
onChange={(e) => setNewArticle({ ...newArticle, MwSt: e.target.value })}
label="MwSt"
type="number"
variant="standard"
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>
}}
/>
</Grid>
<Grid item xs={2}>
<FormControlLabel control={
<Checkbox
label='Sale?'
checked={newArticle.inSale}
onChange={(e) => setNewArticle({ ...newArticle, inSale: !newArticle.inSale })}
inputProps={{ 'aria-label': 'controlled' }}
/>
} label="Sale?" />
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleNewEntryClose}>Abbrechen</Button>
<Button onClick={handleNewEntryAdd}>Hinzufügen</Button>
</DialogActions>
</Dialog>
<DataGrid
rows={tableData}
columns={columns}
editMode="row"
//pageSize={15}
onCellClick={handleDataGridClick}
onCellDoubleClick={handleDataGridDoubleClick}
//onCellKeyDown={handleCellKeyDown}
rowModesModel={rowModesModel}
onRowModesModelChange={(model) => setRowModesModel(model)}
processRowUpdate={processRowUpdate}
experimentalFeatures={{ newEditingApi: true }}
components={{
Toolbar: EditToolbar,
}}
componentsProps={{
toolbar: {
setArticles,
setOpenNewEntryDialog
},
}}
/>
<Grid container spacing={1}>
<Grid item xs={4}>
<FormControl sx={{ m: 1, minWidth: 120 }}>
<InputLabel>Zahlungsart</InputLabel>
<Select
id="payment_type"
value={paymentType}
label="Zahlungsart"
onChange={(e) => {setPaymentType(e.target.value)}}
>
{paymentTypes.map((unit) => (
<MenuItem
key={unit}
value={unit}
//style={getStyles(name, personName, theme)}
>
{unit}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={6}>
<TextField
disabled
fullWidth
margin="dense"
id="Gesamtpreis"
value={totalPrice}
onChange={(e) => setTotalPrice(e.target.value)}
label="Gesamtpreis"
type="number"
variant="standard"
/*
inputProps: object
Attributes applied to the input element.
InputProps: object
Props applied to the Input element. It will be a FilledInput, OutlinedInput or Input component depending on the variant prop value.
*/
InputProps={{ endAdornment: <InputAdornment position="end"></InputAdornment> }}
inputProps={{ style: { textAlign: 'right' } }}
//onBlur={(e) => handleArticleChangeAPI(e,'EAN', e.target.value)}
/>
</Grid>
<Grid item xs={2} alignItems="stretch" style={{ display: "flex" }}>
<Button onClick={handleSavePrice}>
Preis übernehmen
</Button>
</Grid>
</Grid>
{!!snackbar && (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={handleCloseSnackbar}
autoHideDuration={6000}
>
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
</div>
</div>
</Grid>
<Grid item xs={4}>
<ReceipeCanvas
setNewRect={setDrawData}
image={img}
myRef={drawRef}
/>
</Grid>
</Grid>
</div>
)
}
export default PurchaseDetails

View File

@@ -0,0 +1,162 @@
import { Button } from '@mui/material'
import {Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material'
//import {Autocomplete, createFilterOptions } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import { Grid } from '@mui/material'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker';
import Typography from '@mui/material/Typography';
import {useState, useEffect} from 'react'
import React from 'react'
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import SelectMarketField from "./selectMarket";
import UploadFiles from "./upload-files.component";
//const baseURL = "https://jsonplaceholder.typicode.com/comments";
const baseURL = "http://localhost:8000/api"
const PurchaseOverview = (props) => {
const columns = [
{
field: 'edit_finished',
headerName: 'Fertig?',
type: 'boolean',
width: 100,
editable: true,
},
{
field: 'purchase_date',
headerName: 'Datum'
},
{
field: 'market',
headerName: 'Markt',
valueGetter: (params) => { return params.row.market.name + ', ' + params.row.market.street + ' ' + params.row.market.street_number + ', ' + params.row.market.city},
width: 400,
},
{
field: 'total_price',
headerName: 'Gesamtpreis',
flex: 1,
valueFormatter: (params) => {
if (params.value == null) {
return '';
}
//const valueFormatted = Number(params.value * 100).toLocaleString();
return `${params.value}`;
},
},
//{field: 'body', headerName: 'Body'}
]
const navigate = useNavigate();
const [tableData, setTableData] = useState([]);
const [openAddDialog, setOpenAddDialog] = useState(false);
const [selectedDate, setSelectedDate] = React.useState(new Date());
const handleDataGridClick = (e) => {
//console.log(e.row)
navigate('/PurchaseDetails', {state:e.row});
}
const handleClickAddDialogOpen = () => {
setOpenAddDialog(true);
};
const handleClose = () => {
setOpenAddDialog(false);
};
useEffect(()=>{
axios.get(baseURL+"/purchase/")
.then((res) => {
const data = res.data;
console.log(data)
setTableData(data);
})
},[])
return (
<div style={{ height: 800, width: '100%' }}>
<Typography variant="h2" gutterBottom>
Übersicht über alle Einkäufe
</Typography>
<Button variant="contained"
onClick={handleClickAddDialogOpen}
>
Einkauf hinzufügen
</Button>
<Dialog open={openAddDialog} onClose={handleClose}>
<DialogTitle>Einkauf hinzufügen</DialogTitle>
<DialogContent>
<DialogContentText>
Füge einen neuen Einkauf hinzu. Entweder per Datum und Markt oder per Upload eines Kassenzettels.
</DialogContentText>
<Grid container spacing={1}>
<Grid item xs={5}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<MobileDatePicker
label="Datum des Einkaufs"
value={selectedDate}
onChange={(newValue) => {
setSelectedDate(newValue);
}}
renderInput={(params) => <TextField {...params} />}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={6}>
<SelectMarketField/>
</Grid>
<Grid item xs={12}>
<UploadFiles
apiURL='/purchase/post_receipe/'
fileName='receipe'
buttonText='Upload'
applyBinarize={false}
debug={true}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Abbrechen</Button>
<Button onClick={handleClose}>Hinzufügen</Button>
</DialogActions>
</Dialog>
<DataGrid
rows={tableData}
columns={columns}
pageSize={10}
//checkboxSelection
//disableSelectionOnClick
onCellClick={handleDataGridClick}
/>
</div>
)
}
export default PurchaseOverview

View File

@@ -0,0 +1,53 @@
import { Box} from '@mui/material'
import React from 'react'
import { useNavigate } from 'react-router-dom';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
const Sidebar = () => {
const navigate = useNavigate();
const showArticles = (e) => {
console.log('Show articles');
navigate('/Articles', {});
};
const showPurchases = (e) => {
navigate('/', {});
};
const showAnalyses = (e) => {
console.log('Show analyses');
navigate('/Analyses', {});
};
return (
<Box bgcolor="skyblue"
flex={1}
p={2}
sx={{ display: {xs: "none", sm: "block"} }}
>
<Stack spacing={2}>
<Button variant="text"
onClick={showPurchases}
>
Einkäufe
</Button>
<Button variant="text"
onClick={showArticles}
>
Artikel
</Button>
<Button variant="text"
onClick={showAnalyses}
>
Analysen
</Button>
</Stack>
</Box>
)
}
export default Sidebar

View File

@@ -0,0 +1,58 @@
import * as React from 'react';
import {useState} from 'react'
import axios from 'axios';
import Button from '@mui/material/Button';
//import Typography from '@mui/material/Typography';
import {Card, CardActions, CardContent, CardMedia } from '@mui/material'
//import {Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from '@mui/material'
//import FormGroup from '@mui/material/FormGroup';
//import FormControlLabel from '@mui/material/FormControlLabel';
const baseURL = "http://localhost:8000/api"
const ArticleCard = (props) => {
const { article, navigate, onChange, filterBrand } = props;
const [openAddDialog, setOpenAddDialog] = useState(false);
const [error, setError] = useState(false);
const handleClick = () => {
navigate('/ArticleDetails', {state:{id: -1, article_id: article}});
}
return (
<Card sx={{ height: 350, maxWidth: 345, position: "relative"}}>
<CardMedia
component="img"
height="200"
image={article.frontImage}
title="Bild"
sx={{ padding: "1em 1em 0 1em", objectFit: "contain" }}
/>
<CardContent>
<Button variant="text"
sx={{textTransform: "none"}}
onClick={filterBrand}
>
{article.brand === null ? "Unbekannte Marke:": article.brand.name+":"}
</Button>
{article.name}
</CardContent>
<CardActions sx={{position: "absolute", bottom: 0,}}>
<Button size="small" onClick={handleClick}>
Öffnen
</Button>
</CardActions>
</Card>
);
}
export default ArticleCard

View File

@@ -0,0 +1,110 @@
import * as React from 'react';
import {useState} from 'react'
import axios from 'axios';
import {Button, Stack, Input } from '@mui/material';
import Typography from '@mui/material/Typography';
import {Card, CardActions, CardContent, CardMedia } from '@mui/material'
import {Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from '@mui/material'
const baseURL = "http://localhost:8000/api"
const PictureCard = (props) => {
const { type, article, pictureType, onChange } = props;
const [openAddDialog, setOpenAddDialog] = useState(false);
const [uploadFile, setUploadFile] = useState(null);
const [error, setError] = useState(false);
//const [image, setImage] = useState(eval("article."+pictureType));
const handleClick = () => {
setOpenAddDialog(true);
}
const handleClose = () => {
setOpenAddDialog(false);
};
const handleUploadSelection = (e) => {
//console.log(e.target.files[0])
setUploadFile(e.target.files[0]);
};
const handleUpload = (event) => {
event.preventDefault()
const formData = new FormData();
formData.append("fileTitle", pictureType);
formData.append("uploadedFile", uploadFile);
axios.post(baseURL+'/article/post_articleImage/'+article.pk, formData, {
headers: {
'content-type': 'multipart/form-data'
}
})
.then(function (response) {
setOpenAddDialog(false);
//console.log(response);
//setImage(eval("response.data."+pictureType))
onChange(response.data)
})
.catch(function (error) {
//console.log(error);
setError(true);
});
};
return (
<Card sx={{ maxWidth: 345 }}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{type}
</Typography>
</CardContent>
<CardMedia
component="img"
height="450"
image={eval("article."+pictureType)}
title="Bild"
sx={{ padding: "1em 1em 0 1em", objectFit: "contain" }}
/>
<CardActions>
<Button size="small" onClick={handleClick}>
{eval("article."+pictureType) === null ? "Bild hinzufügen" : "Neues Bild"}
</Button>
<Dialog open={openAddDialog} onClose={handleClose}>
<DialogTitle>Bild hinzufügen</DialogTitle>
<DialogContent>
<DialogContentText>
Bild auswählen und uploaden
</DialogContentText>
<Stack container spacing={1}>
<Input
accept="image/*"
id="contained-button-file"
multiple type="file"
onChange={handleUploadSelection}
//error={error.toString()}
/>
<Typography gutterBottom variant="subtitle1" component="div" color="red">
{error ? "Fehler beim uploaden des Files!" : ""}
</Typography>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
Abbrechen
</Button>
<Button onClick={handleUpload}>
Hinzufügen
</Button>
</DialogActions>
</Dialog>
</CardActions>
</Card>
);
}
export default PictureCard

View File

@@ -0,0 +1,56 @@
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* @param blocker
* @param when
* @see https://reactrouter.com/api/useBlocker
*/
export function useBlocker( blocker, when = true ) {
const { navigator } = useContext( NavigationContext );
useEffect( () => {
if ( ! when ) return;
const unblock = navigator.block( ( tx ) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker( autoUnblockingTx );
} );
return unblock;
}, [ navigator, blocker, when ] );
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* @param message
* @param when
*/
export default function usePrompt( message, when = true ) {
const blocker = useCallback(
( tx ) => {
// eslint-disable-next-line no-alert
if ( window.confirm( message ) ) tx.retry();
},
[ message ]
);
useBlocker( blocker, when );
}

View File

@@ -0,0 +1,292 @@
import {Autocomplete, createFilterOptions } from '@mui/material';
import { Button, Grid } from '@mui/material'
import {Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material'
import {React, useState, useEffect, Fragment} from 'react'
import axios from 'axios';
const baseURL = "http://localhost:8000/api"
const filter = createFilterOptions();
const SelectMarketField = () => {
const [value, setValue] = useState(null);
const [open, toggleOpen] = useState(false);
const [optionData, setOptionData] = useState([]);
const handleClose = () => {
//setDialogValue({
// name: '',
// street: '',
//});
console.log(dialogValue)
axios.post(baseURL+'/markets/', {
name: dialogValue.name,
street: dialogValue.street,
street_number: dialogValue.streetNum,
zip_code: dialogValue.zip,
city: dialogValue.city,
phone: dialogValue.phone
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
axios.get(baseURL+"/market")
.then((res) => {
const data = res.data;
setOptionData(data);
})
.then(function (res) {
console.log(res);
})
.catch(function (error) {
console.log(error);
});
toggleOpen(false);
};
const [dialogValue, setDialogValue] = useState({
name: '',
street: '',
streetNum: 0,
zip: '',
city: '',
phone: ''
});
const handleSubmit = (event) => {
event.preventDefault();
setValue({
name: dialogValue.name,
street: dialogValue.street,
streetNum: parseInt(dialogValue.streetNum,10),
zip: parseInt(dialogValue.zip,10),
city: dialogValue.city,
phone: dialogValue.phone
});
handleClose();
};
useEffect(()=>{
axios.get(baseURL+"/markets")
.then((res) => {
const data = res.data;
//console.log(data)
setOptionData(data);
})
},[])
return (
<Fragment>
<Autocomplete
value={value}
onChange={(event, newValue) => {
if (typeof newValue === 'string') {
// timeout to avoid instant validation of the dialog's form.
setTimeout(() => {
toggleOpen(true);
console.log('this')
setDialogValue({
name: newValue,
street: '',
streetNum: 0,
zip: '',
city: '',
phone: ''
});
});
}
else if (newValue && newValue.inputValue) {
toggleOpen(true);
setDialogValue({
name: newValue.inputValue,
street: '',
streetNum: 0,
zip: '',
city: '',
phone: ''
});
}
else {
setValue(newValue);
}
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
if (params.inputValue !== '') {
filtered.push({
inputValue: params.inputValue,
name: `Add "${params.inputValue}"`,
});
}
return filtered;
}}
id="free-solo-dialog"
options={optionData}
getOptionLabel={(option) => {
// e.g value selected with enter, right from the input
if (typeof option === 'string') {
return option;
}
if (option.inputValue) {
return option.inputValue;
}
return option.name;
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
renderOption={(props, option) => <li {...props}>{option.name}</li>}
sx={{ width: 300 }}
freeSolo
renderInput={(params) => <TextField {...params} label="Markt auswählen" />}
/>
<Dialog open={open} onClose={handleClose}>
<form onSubmit={handleSubmit}>
<DialogTitle>Neuen Markt hinzufügen</DialogTitle>
<DialogContent>
<DialogContentText>
Ist der Markt noch nicht in der Liste? Dann füge ihn hinzu.
</DialogContentText>
<Grid container spacing={1}>
<Grid item xs={12}>
<TextField
autoFocus
fullWidth
margin="dense"
id="name"
error={dialogValue.name === ''}
value={dialogValue.name}
onChange={(event) =>
setDialogValue({
...dialogValue,
name: event.target.value,
})
}
label="Name"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={9}>
<TextField
margin="dense"
fullWidth
id="street"
error={dialogValue.street === ''}
//value={dialogValue.street}
onChange={(event) =>
setDialogValue({
...dialogValue,
street: event.target.value,
})
}
label="Straße"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={2}>
<TextField
margin="dense"
id="streetnum"
fullWidth
//value={dialogValue.streetNum}
error={dialogValue.streetNum === 0}
onChange={(event) =>
setDialogValue({
...dialogValue,
streetNum: event.target.value,
})
}
label="Nr."
type="number"
variant="standard"
/>
</Grid>
<Grid item xs={4}>
<TextField
margin="dense"
id="zip"
fullWidth
error={dialogValue.zip === ''}
//helperText="Incorrect entry."
//value={dialogValue.zip}
onChange={(event) =>
setDialogValue({
...dialogValue,
zip: event.target.value,
})
}
label="PLZ"
type="number"
variant="standard"
/>
</Grid>
<Grid item xs={8}>
<TextField
margin="dense"
id="city"
fullWidth
error={dialogValue.city === ''}
//value={dialogValue.city}
onChange={(event) =>
setDialogValue({
...dialogValue,
city: event.target.value,
})
}
label="Stadt"
type="text"
variant="standard"
/>
</Grid>
<Grid item xs={12}>
<TextField
margin="dense"
id="phone"
fullWidth
//value={dialogValue.tel}
onChange={(event) =>
setDialogValue({
...dialogValue,
tel: event.target.value,
})
}
label="Telefon"
type="text"
variant="standard"
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
>
Abbrechen
</Button>
<Button
type="submit"
disabled={dialogValue.name === "" ||
dialogValue.street === "" ||
dialogValue.streetNum === 0 ||
dialogValue.zip === "" ||
dialogValue.city === ""
}
>
Hinzufügen</Button>
</DialogActions>
</form>
</Dialog>
</Fragment>
);
}
export default SelectMarketField

View File

@@ -0,0 +1,102 @@
import React from "react";
import {useState, useEffect} from 'react'
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import LinearProgress from '@mui/material/LinearProgress';
import { Box, Typography, Button } from '@mui/material';
import axios from 'axios';
const baseURL = "http://localhost:8000/api"
const Input = styled('input')({
display: 'none',
});
function LinearProgressWithLabel(props) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value,
)}%`}</Typography>
</Box>
</Box>
);
}
LinearProgressWithLabel.propTypes = {
/**
* The value of the progress indicator for the determinate and buffer variants.
* Value between 0 and 100.
*/
value: PropTypes.number.isRequired,
};
const UploadFiles = (props) => {
const { apiURL, fileName, buttonText, applyBinarize, debug } = props;
const [progress, setProgress] = useState(0);
const [uploadFile, setUploadFile] = useState(0);
const handleUploadSelection = (e) => {
console.log(e.target.files[0])
setUploadFile(e.target.files[0]);
};
const handleUpload = (event) => {
event.preventDefault()
const formData = new FormData();
console.log(uploadFile)
formData.append("fileTitle", fileName);
formData.append("applyBinarize", applyBinarize);
formData.append("debug", debug);
formData.append("uploadedFile", uploadFile);
axios.post(baseURL+apiURL, formData, {
headers: {
'content-type': 'multipart/form-data'
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
};
return(
<div style={{ height: 100, width: '100%' }}>
<Box sx={{ width: '100%' }}>
<LinearProgressWithLabel value={progress} />
</Box>
<label htmlFor="contained-button-file">
<Input
accept="image/*"
id="contained-button-file"
multiple type="file"
onChange={handleUploadSelection}
/>
<Button variant="contained" component="span">
Bild auswählen
</Button>
<div className="file-name">
{uploadFile && uploadFile.length > 0 ? uploadFile[0].name : null}
</div>
<Button
variant="contained"
component="span"
onClick={handleUpload}
>
{buttonText}
</Button>
</label>
</div>
)
}
export default UploadFiles

View File

@@ -0,0 +1,50 @@
/**
* Prompts a user when they exit the page
*/
import { useCallback, useContext, useEffect } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
function useConfirmExit(confirmExit, when = true) {
const navigator = useContext(NavigationContext);
useEffect(() => {
if (!when) {
return;
}
const push = navigator.push;
navigator.push = (...args) => {
const result = confirmExit();
if (result !== false) {
push(...args);
}
};
return () => {
navigator.push = push;
};
}, [navigator, confirmExit, when]);
}
export default function usePrompt(message, when = true) {
useEffect(() => {
if (when) {
window.onbeforeunload = function () {
return message;
};
}
return () => {
window.onbeforeunload = null;
};
}, [message, when]);
const confirmExit = useCallback(() => {
const confirm = window.confirm(message);
return confirm;
}, [message]);
useConfirmExit(confirmExit, when);
}

View File

@@ -0,0 +1,8 @@
import axios from "axios";
export default axios.create({
baseURL: "http://localhost:8000/api",
headers: {
"Content-type": "application/json"
}
});

View File

@@ -0,0 +1,19 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

View File

@@ -0,0 +1,15 @@
import http from "../http-common";
class UploadFilesService {
upload(file, onUploadProgress) {
let formData = new FormData();
formData.append("file", file);
return http.post("/purchase/post_receipe/", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress,
});
}
}
export default new UploadFilesService();

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

11173
reactFrontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff