Project files
This commit is contained in:
23
reactFrontend/.gitignore
vendored
Normal file
23
reactFrontend/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
36
reactFrontend/README.md
Normal file
36
reactFrontend/README.md
Normal 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`.
|
||||
|
||||
57
reactFrontend/package.json
Normal file
57
reactFrontend/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
54
reactFrontend/package.json.old
Normal file
54
reactFrontend/package.json.old
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
reactFrontend/public/favicon.ico
Normal file
BIN
reactFrontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
reactFrontend/public/index.html
Normal file
43
reactFrontend/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>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>
|
||||
BIN
reactFrontend/public/logo192.png
Normal file
BIN
reactFrontend/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
reactFrontend/public/logo512.png
Normal file
BIN
reactFrontend/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
reactFrontend/public/manifest.json
Normal file
25
reactFrontend/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
reactFrontend/public/robots.txt
Normal file
3
reactFrontend/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
reactFrontend/src/App.css
Normal file
38
reactFrontend/src/App.css
Normal 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
54
reactFrontend/src/App.js
Normal 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;
|
||||
8
reactFrontend/src/App.test.js
Normal file
8
reactFrontend/src/App.test.js
Normal 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();
|
||||
});
|
||||
23
reactFrontend/src/MyRoutes.js
Normal file
23
reactFrontend/src/MyRoutes.js
Normal 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
|
||||
221
reactFrontend/src/components/AnalysesOverview.component.jsx
Normal file
221
reactFrontend/src/components/AnalysesOverview.component.jsx
Normal 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
|
||||
535
reactFrontend/src/components/ArticleDetails.component.js
Normal file
535
reactFrontend/src/components/ArticleDetails.component.js
Normal 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
|
||||
65
reactFrontend/src/components/ArticleOverview.component.jsx
Normal file
65
reactFrontend/src/components/ArticleOverview.component.jsx
Normal 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
|
||||
193
reactFrontend/src/components/Canvas.component.js
Normal file
193
reactFrontend/src/components/Canvas.component.js
Normal 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
|
||||
94
reactFrontend/src/components/CategoryTree.component.js
Normal file
94
reactFrontend/src/components/CategoryTree.component.js
Normal 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
|
||||
|
||||
17
reactFrontend/src/components/Home.js
Normal file
17
reactFrontend/src/components/Home.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
362
reactFrontend/src/components/IngredienceDataGrid.component.jsx
Normal file
362
reactFrontend/src/components/IngredienceDataGrid.component.jsx
Normal 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
|
||||
152
reactFrontend/src/components/ListFieldApi.component.js
Normal file
152
reactFrontend/src/components/ListFieldApi.component.js
Normal 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
|
||||
154
reactFrontend/src/components/ListFieldApiwoArticle.component.js
Normal file
154
reactFrontend/src/components/ListFieldApiwoArticle.component.js
Normal 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
|
||||
271
reactFrontend/src/components/MarketDetail.component.js
Normal file
271
reactFrontend/src/components/MarketDetail.component.js
Normal 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
|
||||
|
||||
90
reactFrontend/src/components/Navbar.jsx
Normal file
90
reactFrontend/src/components/Navbar.jsx
Normal 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
|
||||
231
reactFrontend/src/components/NutriDataGrid.component.js
Normal file
231
reactFrontend/src/components/NutriDataGrid.component.js
Normal 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
|
||||
240
reactFrontend/src/components/PackagingDataGrid.component.js
Normal file
240
reactFrontend/src/components/PackagingDataGrid.component.js
Normal 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
|
||||
207
reactFrontend/src/components/PackagingDataGrid2.component.js
Normal file
207
reactFrontend/src/components/PackagingDataGrid2.component.js
Normal 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
|
||||
999
reactFrontend/src/components/PurchaseDetails.component.js
Normal file
999
reactFrontend/src/components/PurchaseDetails.component.js
Normal 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
|
||||
162
reactFrontend/src/components/PurchaseOverview.component.jsx
Normal file
162
reactFrontend/src/components/PurchaseOverview.component.jsx
Normal 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
|
||||
53
reactFrontend/src/components/Sidebar.jsx
Normal file
53
reactFrontend/src/components/Sidebar.jsx
Normal 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
|
||||
58
reactFrontend/src/components/articleCard.component.jsx
Normal file
58
reactFrontend/src/components/articleCard.component.jsx
Normal 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
|
||||
110
reactFrontend/src/components/pictureCard.component.js
Normal file
110
reactFrontend/src/components/pictureCard.component.js
Normal 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
|
||||
56
reactFrontend/src/components/prompts.js
Normal file
56
reactFrontend/src/components/prompts.js
Normal 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 );
|
||||
}
|
||||
292
reactFrontend/src/components/selectMarket.jsx
Normal file
292
reactFrontend/src/components/selectMarket.jsx
Normal 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
|
||||
102
reactFrontend/src/components/upload-files.component.js
Normal file
102
reactFrontend/src/components/upload-files.component.js
Normal 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
|
||||
50
reactFrontend/src/components/usePrompt.js
Normal file
50
reactFrontend/src/components/usePrompt.js
Normal 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);
|
||||
}
|
||||
|
||||
8
reactFrontend/src/http-common.js
Normal file
8
reactFrontend/src/http-common.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import axios from "axios";
|
||||
|
||||
export default axios.create({
|
||||
baseURL: "http://localhost:8000/api",
|
||||
headers: {
|
||||
"Content-type": "application/json"
|
||||
}
|
||||
});
|
||||
19
reactFrontend/src/index.css
Normal file
19
reactFrontend/src/index.css
Normal 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;
|
||||
}
|
||||
17
reactFrontend/src/index.js
Normal file
17
reactFrontend/src/index.js
Normal 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();
|
||||
1
reactFrontend/src/logo.svg
Normal file
1
reactFrontend/src/logo.svg
Normal 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 |
13
reactFrontend/src/reportWebVitals.js
Normal file
13
reactFrontend/src/reportWebVitals.js
Normal 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;
|
||||
15
reactFrontend/src/services/upload-files.service.js
Normal file
15
reactFrontend/src/services/upload-files.service.js
Normal 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();
|
||||
5
reactFrontend/src/setupTests.js
Normal file
5
reactFrontend/src/setupTests.js
Normal 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
11173
reactFrontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user