Initial commit
This commit is contained in:
51
front/src/app/App.js
Normal file
51
front/src/app/App.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import {CssBaseline, GlobalStyles} from "@mui/material";
|
||||
import {LocalizationProvider} from "../components/core/LocalizationProvider";
|
||||
import {AuthProvider} from "../context/AuthContext";
|
||||
import {createTTheme} from "../styles/theme/create-theme";
|
||||
import {Experimental_CssVarsProvider as CssVarsProvider} from '@mui/material/styles';
|
||||
import {BrowserRouter as Router} from "react-router-dom";
|
||||
import {NavigationRoutes} from "./NavigationRoutes";
|
||||
import {SnackbarProvider} from 'notistack';
|
||||
import {UserProvider} from "../context/UserContext";
|
||||
|
||||
function App() {
|
||||
const theme = createTTheme();
|
||||
|
||||
return (
|
||||
// Провайдер времени
|
||||
<LocalizationProvider>
|
||||
{/*Провайдер уведомлений*/}
|
||||
<SnackbarProvider maxSnack={6} anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
|
||||
style={{borderRadius: '10px'}}>
|
||||
{/*Провайдер авторизации*/}
|
||||
<AuthProvider>
|
||||
{/*Провайдер пользователя*/}
|
||||
<UserProvider>
|
||||
{/*Провайдер темы*/}
|
||||
<CssVarsProvider theme={theme}>
|
||||
<CssBaseline/>
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
body: {
|
||||
'--MainNav-height': '56px',
|
||||
'--MainNav-zIndex': 1000,
|
||||
'--SideNav-width': '280px',
|
||||
'--SideNav-zIndex': 1200,
|
||||
'--MobileNav-width': '320px',
|
||||
'--MobileNav-zIndex': 1200,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/*Маршрутизация*/}
|
||||
<Router>
|
||||
<NavigationRoutes/>
|
||||
</Router>
|
||||
</CssVarsProvider>
|
||||
</UserProvider>
|
||||
</AuthProvider>
|
||||
</SnackbarProvider>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
10
front/src/app/HomeRedirect.js
Normal file
10
front/src/app/HomeRedirect.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {paths} from "../path";
|
||||
import {Loading} from "../components/core/Loading";
|
||||
|
||||
export function HomeRedirect({auth}) {
|
||||
const redirectPath = auth ? paths.dashboard.overview : paths.auth.signIn;
|
||||
window.location.replace(redirectPath);
|
||||
return (
|
||||
<Loading loading={true}/>
|
||||
)
|
||||
}
|
||||
151
front/src/app/NavigationRoutes.js
Normal file
151
front/src/app/NavigationRoutes.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import {Route, Routes} from "react-router-dom";
|
||||
import {paths} from "../path";
|
||||
import {useAuth} from "../hooks/useAuth";
|
||||
import NotFoundPage from "./pages/notFound/NotFoundPage";
|
||||
import {UserLayout} from "./layout/UserLayout";
|
||||
import {HomeRedirect} from "./HomeRedirect";
|
||||
import {PublicLayout} from "./layout/PublicLayout";
|
||||
import QueuePage from "./pages/queue/QueuePage";
|
||||
import LoginPage from "./pages/auth/sign-in/loginPage";
|
||||
import {TelegramCode} from "./pages/auth/sign-in/telegram-code";
|
||||
import {IngredientsPage} from "./pages/ingredients/IngredientsPage";
|
||||
import {MenuPage} from "./pages/cocktails/MenuPage";
|
||||
import {AllCocktailsPage} from "./pages/cocktails/AllCocktailsPage";
|
||||
import {EditIngredientPage} from "./pages/ingredients/EditIngredientPage";
|
||||
import {EditCocktailPage} from "./pages/cocktails/EditCocktailPage";
|
||||
import {MyQueuePage} from "./pages/queue/MyQueuePage";
|
||||
import {VisitorPage} from "./pages/VisitorPage";
|
||||
import {CocktailMenuBarPage} from "./pages/cocktails/CocktailMenuBarPage";
|
||||
import {MyBarPage} from "./pages/MyBarPage";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
export function NavigationRoutes() {
|
||||
const {auth} = useAuth();
|
||||
const [loadedRoutes, setLoadedRoutes] = useState(undefined);
|
||||
useEffect(() => {
|
||||
setLoadedRoutes(auth ? authPages : guestPages)
|
||||
}, [auth]);
|
||||
if (!loadedRoutes) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Routes>
|
||||
{loadedRoutes.map((page) => {
|
||||
return (
|
||||
<Route
|
||||
key={page.path + page.isPrivate + page.exact}
|
||||
path={page.path}
|
||||
exact={page.exact}
|
||||
element={<ElementProvider isPrivate={page.isPrivate}>
|
||||
{page.children}
|
||||
</ElementProvider>}/>
|
||||
)
|
||||
})}
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
function ElementProvider({isPrivate, children}) {
|
||||
if (isPrivate) {
|
||||
return (<UserLayout>{children}</UserLayout>);
|
||||
} else {
|
||||
return (<PublicLayout>{children}</PublicLayout>);
|
||||
}
|
||||
}
|
||||
|
||||
const authPages = [
|
||||
{
|
||||
children: (<HomeRedirect auth={true}/>),
|
||||
isPrivate: false,
|
||||
path: paths.home,
|
||||
},
|
||||
{
|
||||
path: paths.auth.signIn,
|
||||
children: (<LoginPage/>),
|
||||
isPrivate: false,
|
||||
},
|
||||
{
|
||||
path: paths.dashboard.overview,
|
||||
isPrivate: true,
|
||||
children: (<MenuPage/>),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: paths.bar.cocktails,
|
||||
isPrivate: true,
|
||||
children: (<AllCocktailsPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.list,
|
||||
isPrivate: true,
|
||||
children: (<MyBarPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.orders.my,
|
||||
isPrivate: true,
|
||||
children: (<MyQueuePage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.ingredients,
|
||||
isPrivate: true,
|
||||
children: (<IngredientsPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.ordersQueue,
|
||||
isPrivate: true,
|
||||
children: (<QueuePage/>),
|
||||
},
|
||||
{
|
||||
path: paths.visitor.inBar,
|
||||
isPrivate: true,
|
||||
children: (<VisitorPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.ingredientEdit,
|
||||
isPrivate: true,
|
||||
forAdmin: true,
|
||||
children: (<EditIngredientPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.menu,
|
||||
isPrivate: true,
|
||||
children: (<CocktailMenuBarPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.cocktailEdit,
|
||||
isPrivate: true,
|
||||
forAdmin: true,
|
||||
children: (<EditCocktailPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.notFound,
|
||||
isPrivate: false,
|
||||
children: (<NotFoundPage/>)
|
||||
},
|
||||
]
|
||||
|
||||
const guestPages = [
|
||||
{
|
||||
path: paths.home,
|
||||
isPrivate: false,
|
||||
children: (<HomeRedirect auth={false}/>),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: paths.auth.tg,
|
||||
isPrivate: false,
|
||||
children: (<TelegramCode/>),
|
||||
exact: false
|
||||
},
|
||||
{
|
||||
path: paths.auth.signIn,
|
||||
isPrivate:
|
||||
false,
|
||||
children: (<LoginPage/>),
|
||||
},
|
||||
{
|
||||
path: paths.notFound,
|
||||
isPrivate: false,
|
||||
children: (<NotFoundPage/>),
|
||||
},
|
||||
]
|
||||
58
front/src/app/layout/PublicLayout.js
Normal file
58
front/src/app/layout/PublicLayout.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {DynamicLogo} from "../../components/core/Logo";
|
||||
import {paths} from "../../path";
|
||||
|
||||
export function PublicLayout({ children }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: 'flex', lg: 'grid' },
|
||||
flexDirection: 'column',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flex: '1 1 auto', flexDirection: 'column' }}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box component={'a'} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ alignItems: 'center', display: 'flex', flex: '1 1 auto', justifyContent: 'center', p: 3 }}>
|
||||
<Box sx={{ maxWidth: '450px', width: '100%' }}>{children}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
background: 'radial-gradient(50% 50% at 50% 50%, #122647 0%, #090E23 100%)',
|
||||
color: 'var(--mui-palette-common-white)',
|
||||
display: { xs: 'none', lg: 'flex' },
|
||||
justifyContent: 'center',
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={1}>
|
||||
<Typography color="inherit" sx={{ fontSize: '24px', lineHeight: '32px', textAlign: 'center' }} variant="h1">
|
||||
<Box component="span" sx={{ color: '#15b79e' }}>
|
||||
Добро пожаловать в бар
|
||||
</Box>
|
||||
</Typography>
|
||||
<Typography align="center" variant="subtitle1">
|
||||
Самый большой выбор честно спизженных коктейлей
|
||||
</Typography>
|
||||
<Box
|
||||
component="img"
|
||||
alt="Under development"
|
||||
src="/assets/qr.png"
|
||||
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
31
front/src/app/layout/UserLayout.js
Normal file
31
front/src/app/layout/UserLayout.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import {SideNav} from "../../components/navigation/SideNav";
|
||||
import Box from "@mui/material/Box";
|
||||
import {MainNav} from "../../components/navigation/MainNav";
|
||||
import Container from "@mui/material/Container";
|
||||
|
||||
export function UserLayout({children}) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-default)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
minHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<SideNav/>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexDirection: 'column',
|
||||
pl: {xl: 'var(--SideNav-width)'}
|
||||
}}>
|
||||
<MainNav/>
|
||||
<Container maxWidth="xl" sx={{py: '16px'}}>
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
62
front/src/app/pages/MyBarPage.js
Normal file
62
front/src/app/pages/MyBarPage.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import {FormControl, InputAdornment, InputLabel, OutlinedInput, Tabs} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import {a11yProps} from "../../components/core/tabProps";
|
||||
import {CustomTabPanel} from "../../components/core/TabPanel";
|
||||
import {BarList} from "../../components/bar/BarList";
|
||||
|
||||
export function MyBarPage() {
|
||||
const [value, setValue] = React.useState(0);
|
||||
const handleChange = (event, newValue) => setValue(newValue);
|
||||
const [findString, setFindString] = useState("");
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/*Заголовок*/}
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Мои бары</Typography>
|
||||
</Toolbar>
|
||||
{/*Поиск*/}
|
||||
<Paper elevation={6} sx={{my: 2}}>
|
||||
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
|
||||
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||
<OutlinedInput
|
||||
onChange={(e) => setFindString(e.target.value)}
|
||||
label="With normal TextField"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconButton edge="end">
|
||||
<SearchIcon/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Paper>
|
||||
{/*Рабочее поле ингредиентов*/}
|
||||
<Box>
|
||||
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
|
||||
<Tab label="Мои бары" {...a11yProps(0)} />
|
||||
<Tab label="Список" {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box>
|
||||
<CustomTabPanel value={value} index={0}>
|
||||
<BarList all={false} find={findString}/>
|
||||
</CustomTabPanel>
|
||||
<CustomTabPanel value={value} index={1}>
|
||||
<BarList all={true} find={findString}/>
|
||||
</CustomTabPanel>
|
||||
</Box>
|
||||
{/*Модальное окно информации об ингредиенте*/}
|
||||
{/*<IngredientInfoModal ingredient={selectedInfo} open={openModal} closeHandler={handleCloseModal}/>*/}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
77
front/src/app/pages/VisitorPage.js
Normal file
77
front/src/app/pages/VisitorPage.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import {useEffect, useState} from "react";
|
||||
import {api} from "../../lib/clients/api";
|
||||
import {requests} from "../../requests";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {VisitorItem} from "../../components/visitor/VisitorItem";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import * as React from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
|
||||
export function VisitorPage() {
|
||||
const {session, checkSession} = useUser();
|
||||
const [visitors, setVisitors] = useState([])
|
||||
const [open, setOpen] = useState(false);
|
||||
const {createError} = useAlert();
|
||||
|
||||
useEffect(() => {
|
||||
api().get(requests.visitors.all)
|
||||
.then((r) => {
|
||||
setVisitors(r.data)
|
||||
})
|
||||
.catch(() => createError("Ошибка получения данных"))
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setOpen(session.isActive);
|
||||
}, [session, checkSession])
|
||||
|
||||
const changeHandler = (visitor) => {
|
||||
const arr = visitors.map((v) => {
|
||||
if(v.id === visitor.id) {
|
||||
return {
|
||||
...visitor,
|
||||
invited: !visitor.invited
|
||||
}
|
||||
}
|
||||
return v;
|
||||
})
|
||||
api().post(`${requests.visitors.invite}id=${visitor.id}&value=${!visitor.invited}`)
|
||||
.then(() => setVisitors(arr))
|
||||
.catch(() => createError("Ошибка запроса"))
|
||||
|
||||
}
|
||||
const changeShift = () => {
|
||||
api().post(`${requests.bar.session.change}?value=${!open}`)
|
||||
.then(() => {
|
||||
checkSession?.();
|
||||
setOpen(!open)
|
||||
})
|
||||
.catch(() => createError("Ошибка закрытия сессии"))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/*Заголовок*/}
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Посетители</Typography>
|
||||
</Toolbar>
|
||||
<Box ml={0} mb={2}>
|
||||
{visitors.map((v) => {
|
||||
return (
|
||||
<VisitorItem key={v.id} visitor={v} changeHandler={changeHandler} open={open}/>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant='contained'
|
||||
color={open ? 'error' : 'success'}
|
||||
onClick={() => changeShift()}
|
||||
>{`${open ? "Закрыть " : "Открыть "}смену`}</Button>
|
||||
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
11
front/src/app/pages/auth/sign-in/loginPage.js
Normal file
11
front/src/app/pages/auth/sign-in/loginPage.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import {GuestGuard} from "../../../../components/auth/guest-guard";
|
||||
import {SignInForm} from "../../../../components/auth/sign-in-form";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<GuestGuard>
|
||||
<SignInForm/>
|
||||
</GuestGuard>
|
||||
);
|
||||
}
|
||||
30
front/src/app/pages/auth/sign-in/telegram-code.js
Normal file
30
front/src/app/pages/auth/sign-in/telegram-code.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react";
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
import {Loading} from "../../../../components/core/Loading";
|
||||
import {api} from "../../../../lib/clients/api";
|
||||
import {requests} from "../../../../requests";
|
||||
import {useAuth} from "../../../../hooks/useAuth";
|
||||
|
||||
export function TelegramCode() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const {checkSession} = useAuth();
|
||||
|
||||
let code = searchParams.get("code");
|
||||
const request = {
|
||||
byLogin: false,
|
||||
code: code
|
||||
}
|
||||
api().post(requests.auth.login, request)
|
||||
.then(async (response) => {
|
||||
if (response.data.error) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("token", response.data.token);
|
||||
await checkSession?.();
|
||||
window.location.reload();
|
||||
})
|
||||
|
||||
return (
|
||||
<Loading loading={true}/>
|
||||
)
|
||||
}
|
||||
7
front/src/app/pages/cocktails/AllCocktailsPage.js
Normal file
7
front/src/app/pages/cocktails/AllCocktailsPage.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import CocktailsPageContent from "./CocktailsPageContent";
|
||||
|
||||
export function AllCocktailsPage() {
|
||||
return (
|
||||
<CocktailsPageContent all={true}/>
|
||||
)
|
||||
}
|
||||
127
front/src/app/pages/cocktails/CocktailMenuBarPage.js
Normal file
127
front/src/app/pages/cocktails/CocktailMenuBarPage.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import {Fab, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import {blue} from "@mui/material/colors";
|
||||
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import {Loading} from "../../../components/core/Loading";
|
||||
import * as React from "react";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {CocktailsList} from "../../../components/cocktails/CocktailsList";
|
||||
import {requests} from "../../../requests";
|
||||
import {api} from "../../../lib/clients/api";
|
||||
import {useAlert} from "../../../hooks/useAlert";
|
||||
import {CocktailInfoModal} from "../../../components/cocktails/CocktailInfoModal";
|
||||
|
||||
export function CocktailMenuBarPage() {
|
||||
const {createError} = useAlert();
|
||||
const [grouping, setGrouping] = useState(true);
|
||||
const [findString, setFindString] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [cocktails, setCocktails] = useState([]);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
api().get(`${requests.cocktails.menu}?all=true`)
|
||||
.then((r) => {
|
||||
setCocktails(r.data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => createError("Ошибка получения данных"))
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
|
||||
const handleOpenModal = (row) => {
|
||||
setSelected(row)
|
||||
setOpenModal(true);
|
||||
}
|
||||
const changeHandler = (row, value) => {
|
||||
const newState = cocktails.map((r) => {
|
||||
if(r.id !== row.id) {
|
||||
return r;
|
||||
}
|
||||
return {
|
||||
...r,
|
||||
inMenu: value
|
||||
}
|
||||
});
|
||||
api().post(`${requests.cocktails.menu}?id=${row.id}&value=${value}`)
|
||||
.then(() => {
|
||||
setCocktails(newState);
|
||||
}).catch(() => createError("Ошибка сохранения данных"))
|
||||
}
|
||||
|
||||
const visibleRows = useMemo(() => {
|
||||
if (findString === "") {
|
||||
return cocktails;
|
||||
}
|
||||
let regExp = new RegExp("(.*?)" + findString + "(.*?)", "i");
|
||||
return cocktails
|
||||
.filter((row) => row.name.split(" ").map((n) => n.match(regExp) !== null).includes(true))
|
||||
// eslint-disable-next-line
|
||||
}, [cocktails, findString])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/*Заголовок*/}
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Меню бара</Typography>
|
||||
</Toolbar>
|
||||
{/*Поиск*/}
|
||||
<Paper elevation={6} sx={{my: 2}}>
|
||||
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
|
||||
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||
<OutlinedInput
|
||||
onChange={(e) => setFindString(e.target.value)}
|
||||
label="With normal TextField"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconButton edge="end">
|
||||
<SearchIcon/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControlLabel sx={{ml: '2px'}}
|
||||
control={<Switch defaultChecked/>}
|
||||
onClick={() => setGrouping(!grouping)}
|
||||
label="Группировать"
|
||||
labelPlacement="end"/>
|
||||
</Paper>
|
||||
{/*Рабочее поле коктейлей*/}
|
||||
<CocktailsList rows={visibleRows} changeHandler={changeHandler}
|
||||
infoHandler={handleOpenModal} grouping={grouping}/>
|
||||
{/*Иконка возврата наверх*/}
|
||||
<Fab sx={{
|
||||
alpha: '30%',
|
||||
position: 'sticky',
|
||||
bottom: '16px',
|
||||
color: 'common.white',
|
||||
bgcolor: blue[600],
|
||||
'&:hover': {
|
||||
bgcolor: blue[600],
|
||||
},
|
||||
}}
|
||||
onClick={() => window.window.scrollTo(0, 0)}
|
||||
aria-label='Expand'
|
||||
color='inherit'>
|
||||
<UpIcon/>
|
||||
</Fab>
|
||||
{/*Загрузчик*/}
|
||||
<Loading loading={loading}/>
|
||||
{/*Модальное окно информации об ингредиенте*/}
|
||||
<CocktailInfoModal open={openModal} row={selected}
|
||||
closeHandler={() => {
|
||||
setSelected(null);
|
||||
setOpenModal(false);
|
||||
}}/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
333
front/src/app/pages/cocktails/CocktailsPageContent.js
Normal file
333
front/src/app/pages/cocktails/CocktailsPageContent.js
Normal file
@@ -0,0 +1,333 @@
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {useAlert} from "../../../hooks/useAlert";
|
||||
import * as React from "react";
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import {Cocktail} from "../../../components/cocktails/Cocktail";
|
||||
import {Fab, Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import {requests} from "../../../requests";
|
||||
import {NoResult} from "../../../components/cocktails/NoResult";
|
||||
import {FilterBlock} from "../../../components/cocktails/FilterBlock";
|
||||
import {api} from "../../../lib/clients/api";
|
||||
import {CocktailInfoModal} from "../../../components/cocktails/CocktailInfoModal";
|
||||
import {useUser} from "../../../hooks/useUser";
|
||||
import {blue} from "@mui/material/colors";
|
||||
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import {sortList} from "../../../components/cocktails/sortingList";
|
||||
import {getComparator} from "../../../components/core/getComparator";
|
||||
import Button from "@mui/material/Button";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import CheckMarks from "../../../components/cocktails/CheckMarks";
|
||||
|
||||
const filterList = (rows, filter, allowIngredients) => {
|
||||
let regExp = new RegExp("(.*?)" + filter.search + "(.*?)", "i");
|
||||
const sortingObj = sortList.find((s) => s.name === filter.sorting);
|
||||
const sortingValues = sortingObj.id.split("|");
|
||||
return rows
|
||||
.filter((row) => {
|
||||
const nameReg = row.name.split(" ").map((n) => n.match(regExp) !== null).includes(true);
|
||||
const ingredientReg = row.components
|
||||
.split(", ")
|
||||
.map((r) => r.match(regExp) !== null)
|
||||
.includes(true);
|
||||
return nameReg || ingredientReg;
|
||||
})
|
||||
.filter((row) => filter.onlyFavourite ? row.rating.favourite : true)
|
||||
.filter((row) => filter.glass.length === 0 || filter.glass.includes(row.glass))
|
||||
.filter((row) => filter.category.length === 0 || filter.category.includes(row.category))
|
||||
.filter((row) => filter.alcohol.length === 0 || filter.alcohol.includes(row.alcoholic))
|
||||
.filter((row) => {
|
||||
if (filter.tags.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (row.tags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return row.tags.split(",").find((tag) => filter.tags.includes(tag))
|
||||
})
|
||||
.filter((row) => {
|
||||
if (filter.iCount.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const arr = row.components.split(", ");
|
||||
const count = arr.filter((n) => !allowIngredients.includes(n)).length;
|
||||
const filt = filter.ingredient.length === 0 || arr.filter((n) => filter.ingredient.includes(n)).length > 0;
|
||||
|
||||
return filter.iCount === count && filt;
|
||||
})
|
||||
.filter((row) => {
|
||||
if (filter.inMenu === "") {
|
||||
return row;
|
||||
}
|
||||
const filterValue = filter.inMenu === "Есть в меню";
|
||||
return filterValue === row.inMenu;
|
||||
})
|
||||
.sort(getComparator(sortingValues[1], sortingValues[0], "name"))
|
||||
}
|
||||
|
||||
const emptyFilter = {
|
||||
search: "",
|
||||
hidden: true,
|
||||
onlyFavourite: false,
|
||||
glass: [],
|
||||
category: [],
|
||||
alcohol: [],
|
||||
tags: [],
|
||||
iCount: [],
|
||||
ingredient: [],
|
||||
inMenu: "",
|
||||
sorting: "Название по возрастанию"
|
||||
}
|
||||
|
||||
const CocktailsPageContent = ({all}) => {
|
||||
const {user} = useUser();
|
||||
const {createError, createSuccess} = useAlert();
|
||||
const [allowIngredients, setAllowIngredients] = useState([])
|
||||
const [rows, setRows] = useState([]);
|
||||
const [filter, setFilter] = useState(emptyFilter)
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedCocktail, setSelectedCocktail] = useState(null)
|
||||
const [chips, setChips] = useState([])
|
||||
const [page, setPage] = useState(-1);
|
||||
const [load, setLoad] = useState(false);
|
||||
const [isEnd, setIsEnd] = useState(false);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
|
||||
const loading = useCallback(() => {
|
||||
const size = Math.floor((window.innerWidth) / 350) * 5;
|
||||
if (load || (!isNew && isEnd)) {
|
||||
return false;
|
||||
}
|
||||
setLoad(true);
|
||||
const request = {
|
||||
...filter,
|
||||
all: all,
|
||||
sort: sortList.find((s) => s.name === filter.sorting).id,
|
||||
page: page + 1,
|
||||
size: size,
|
||||
iCount: Array.isArray(filter.iCount) ? null : filter.iCount
|
||||
}
|
||||
api().post(requests.cocktails.menu, request)
|
||||
.then((r) => {
|
||||
if (r.data.length === 0) {
|
||||
if(isNew) {
|
||||
setRows([]);
|
||||
}
|
||||
setIsEnd(true);
|
||||
setLoad(false);
|
||||
return;
|
||||
}
|
||||
const cocktails = isNew ? r.data : rows.concat(r.data);
|
||||
setRows(cocktails);
|
||||
setIsNew(false);
|
||||
setPage(page + 1);
|
||||
setLoad(false);
|
||||
})
|
||||
.catch((r) => {
|
||||
setLoad(false);
|
||||
createError("Ошибка загрузки данных от сервера Status:" + r.status)
|
||||
})
|
||||
// eslint-disable-next-line
|
||||
}, [load, isEnd, page]);
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const {scrollTop, scrollHeight, clientHeight} = document.documentElement;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||
loading();
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [loading]);
|
||||
useEffect(() => {
|
||||
api().get(requests.bar.ingredientSimple)
|
||||
.then((r) => {
|
||||
const arr = r.data.filter((i) => i.isHave)
|
||||
.map((i) => i.name)
|
||||
setAllowIngredients(arr)
|
||||
})
|
||||
.catch(() => createError("Ошибка получения ингредиентов"))
|
||||
// eslint-disable-next-line
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
loading();
|
||||
}, [filter])
|
||||
useEffect(() => {
|
||||
if (!all) {
|
||||
return;
|
||||
}
|
||||
const ingredients = new Set();
|
||||
rows.map((c) => c.components)
|
||||
.map((c) => c.split(", "))
|
||||
.map((c) => c.filter((i) => !allowIngredients.includes(i)))
|
||||
.filter((nhc) => nhc.length === 1)
|
||||
.map((fc) => fc[0])
|
||||
.forEach((i) => ingredients.add(i))
|
||||
setChips(Array.from(ingredients).sort(getComparator()));
|
||||
}, [rows, allowIngredients])
|
||||
|
||||
const renderSkeleton = () => {
|
||||
return Array.from({length: 3}, () => null)
|
||||
.map((v, index) => <Skeleton sx={{m: 2}}
|
||||
key={index}
|
||||
variant="rounded"
|
||||
width={350}
|
||||
height={690}/>);
|
||||
}
|
||||
const handleChangeRating = (row, value) => {
|
||||
const newState = rows.map((r) => {
|
||||
if (row.id === r.id) {
|
||||
let newRating = r.rating;
|
||||
newRating.rating = value
|
||||
return {
|
||||
...r,
|
||||
rating: newRating
|
||||
}
|
||||
}
|
||||
return r;
|
||||
})
|
||||
api().post(`${requests.cocktails.rating}${row.id}&rating=${value}`)
|
||||
.then(() => {
|
||||
setRows(newState);
|
||||
createSuccess("Спасибо за оценку!")
|
||||
}).catch(() => createError("Ошибка сохранения"))
|
||||
|
||||
}
|
||||
const handleFilterChange = (filterName, value) => {
|
||||
const newState = {
|
||||
...filter,
|
||||
[filterName]: value
|
||||
}
|
||||
setFilter(newState)
|
||||
setIsNew(true);
|
||||
setIsEnd(false);
|
||||
setPage(-1);
|
||||
}
|
||||
const handleFavourite = (row) => {
|
||||
const value = !row.rating.favourite;
|
||||
const newState = rows.map((r) => {
|
||||
if (r.id === row.id) {
|
||||
let newRating = r.rating;
|
||||
newRating.favourite = value;
|
||||
return {
|
||||
...r,
|
||||
rating: newRating
|
||||
}
|
||||
}
|
||||
return r;
|
||||
});
|
||||
let url = `${requests.cocktails.favourite}${row.id}`;
|
||||
let request = value ? api().put(url) : api().delete(url);
|
||||
|
||||
request
|
||||
.then(() => {
|
||||
setRows(newState);
|
||||
createSuccess("Спасибо за оценку!")
|
||||
}).catch(() => createError("Ошибка сохранения"))
|
||||
}
|
||||
const handleFilterClear = () => {
|
||||
setFilter(emptyFilter);
|
||||
}
|
||||
const handleSelectCocktail = (row) => {
|
||||
setSelectedCocktail(row.id)
|
||||
setOpen(true)
|
||||
}
|
||||
const handleCloseCocktailModal = () => {
|
||||
setOpen(false);
|
||||
setSelectedCocktail(null);
|
||||
}
|
||||
const handleEditMenu = (row, value) => {
|
||||
const newState = rows.map((r) => {
|
||||
if (r.id !== row.id) {
|
||||
return r;
|
||||
}
|
||||
if (all) {
|
||||
return {
|
||||
...r,
|
||||
inMenu: value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}).filter((r) => r !== null);
|
||||
|
||||
api().post(`${requests.cocktails.menu}?id=${row.id}&value=${value}`)
|
||||
.then(() => setRows(newState))
|
||||
.catch(() => createError("Ошибка сохранения данных"))
|
||||
}
|
||||
const editMenuBlock = (row) => {
|
||||
if (user.role === "USER" || user.role === "ADMIN_NOT_BARMEN") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button color={row.inMenu ? 'error' : 'success'} variant='contained'
|
||||
onClick={() => handleEditMenu(row, !row.inMenu)}>
|
||||
{(row.inMenu ? 'Удалить из' : 'Добавить в') + ' меню'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/*<Loading loading={load}/>*/}
|
||||
{/*Модальное окно информации о коктейле*/}
|
||||
<CocktailInfoModal row={selectedCocktail} open={open}
|
||||
closeHandler={handleCloseCocktailModal}/>
|
||||
{/*Блок фильтров*/}
|
||||
<FilterBlock
|
||||
filter={filter}
|
||||
handleFilterChange={handleFilterChange}
|
||||
handleClearFilter={handleFilterClear}
|
||||
barmen={user.role !== 'USER'}
|
||||
all={all}
|
||||
/>
|
||||
|
||||
{/*todo: доделать фильтр по количеству недостающих ингредиентов*/}
|
||||
{/*{*/}
|
||||
{/* (all && filter.iCount === 1) && (*/}
|
||||
{/* <Paper sx={{mt: 1}}>*/}
|
||||
{/* <CheckMarks rows={chips} name={"Выбор ингредиента"} filterName={"ingredient"}*/}
|
||||
{/* filterValue={filter.ingredient}*/}
|
||||
{/* handleChange={handleFilterChange}*/}
|
||||
{/* identity*/}
|
||||
{/* />*/}
|
||||
{/* </Paper>*/}
|
||||
{/* )*/}
|
||||
{/*}*/}
|
||||
<Box>
|
||||
{/*Основное содержимое*/}
|
||||
<Grid container rowSpacing={2} columnSpacing={{xs: 1, sm: 1, md: 2}} sx={{m: 1}}>
|
||||
{rows.length > 0 && rows.map((row) => {
|
||||
return (
|
||||
<Cocktail key={row.id} row={row} handleFavourite={handleFavourite}
|
||||
handleChangeRating={handleChangeRating}
|
||||
handleSelect={handleSelectCocktail}
|
||||
editMenuBlock={editMenuBlock}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{load && renderSkeleton()}
|
||||
{rows.length === 0 && (<NoResult/>)}
|
||||
</Grid>
|
||||
</Box>
|
||||
<Fab sx={{
|
||||
alpha: '30%',
|
||||
position: 'sticky',
|
||||
left: 'calc(100% - 16px)',
|
||||
bottom: '16px',
|
||||
color: 'common.white',
|
||||
bgcolor: blue[600],
|
||||
'&:hover': {
|
||||
bgcolor: blue[600],
|
||||
},
|
||||
}}
|
||||
onClick={() => window.window.scrollTo(0, 0)}
|
||||
aria-label='Expand'
|
||||
color='inherit'>
|
||||
<UpIcon/>
|
||||
</Fab>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CocktailsPageContent;
|
||||
258
front/src/app/pages/cocktails/EditCocktailPage.js
Normal file
258
front/src/app/pages/cocktails/EditCocktailPage.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import {Autocomplete} from "@mui/material";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {api} from "../../../lib/clients/api";
|
||||
import {requests} from "../../../requests";
|
||||
import {useAlert} from "../../../hooks/useAlert";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import CheckMarks from "../../../components/cocktails/CheckMarks";
|
||||
import {EditCocktailReceipt} from "../../../components/cocktails/EditCocktailReceipt";
|
||||
import {SelectEdit} from "../../../components/cocktails/SelectEdit";
|
||||
import {getComparator} from "../../../components/core/getComparator";
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
import {Loading} from "../../../components/core/Loading";
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||
import {styled} from "@mui/material/styles";
|
||||
|
||||
const emptyCocktail = {
|
||||
id: null,
|
||||
name: "",
|
||||
alcoholic: "",
|
||||
category: "",
|
||||
components: "",
|
||||
glass: "",
|
||||
image: "",
|
||||
instructions: "",
|
||||
isAllowed: false,
|
||||
rating: {
|
||||
rating: 0,
|
||||
favourite: false
|
||||
},
|
||||
receipt: [],
|
||||
tags: "",
|
||||
video: ""
|
||||
};
|
||||
const alcohol = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Алкогольный"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Безалкогольный",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Опционально"
|
||||
}
|
||||
]
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
height: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
});
|
||||
|
||||
export function EditCocktailPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const {createError, createSuccess, getError} = useAlert();
|
||||
const [cocktails, setCocktails] = useState([]);
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [cocktail, setCocktail] = useState(emptyCocktail);
|
||||
|
||||
const [glass, setGlass] = useState([]);
|
||||
const [category, setCategory] = useState([]);
|
||||
const [tags, setTags] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
api().get(requests.cocktails.simple)
|
||||
.then((r) => {
|
||||
const arr = r.data.sort(getComparator("asc", "name"));
|
||||
setCocktails(arr)
|
||||
|
||||
const currentId = searchParams.get("id");
|
||||
if (!currentId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const currentCocktail = arr.find((r) => r.id === (currentId * 1));
|
||||
if (!currentCocktail) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setSelected(currentCocktail.id);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => createError("Ошибка получения данных"))
|
||||
|
||||
api().get(requests.bar.category)
|
||||
.then((r) => setCategory(r.data.sort(getComparator("asc", "name"))))
|
||||
.catch(() => createError("Ошибка получения категорий"))
|
||||
|
||||
api().get(requests.bar.glass)
|
||||
.then((r) => setGlass(r.data.sort(getComparator("asc", "name"))))
|
||||
.catch(() => createError("Ошибка получения посуды"))
|
||||
|
||||
api().get(requests.bar.tags)
|
||||
.then((r) => setTags(r.data.sort(getComparator("asc", "name"))))
|
||||
.catch(() => createError("Ошибка получения тегов"))
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setCocktail(emptyCocktail);
|
||||
return;
|
||||
}
|
||||
api().get(requests.cocktails.cocktail + selected)
|
||||
.then((r) => {
|
||||
setCocktail(r.data)
|
||||
})
|
||||
.catch(() => getError());
|
||||
// eslint-disable-next-line
|
||||
}, [selected])
|
||||
|
||||
const changeCocktailValue = (name, value) => {
|
||||
if (name === "tags") {
|
||||
value = value.join(",");
|
||||
}
|
||||
setCocktail((prev) => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
}
|
||||
const saveHandler = () => {
|
||||
api().patch(requests.cocktails.edit, cocktail)
|
||||
.then((r) => {
|
||||
if (!r.data.error) {
|
||||
createSuccess("Сохранено")
|
||||
return;
|
||||
}
|
||||
createError("Ошибка на сервере: " + r.data.error)
|
||||
})
|
||||
.catch(() => createError("Неизвестная ошибка"))
|
||||
}
|
||||
const deleteHandle = () => {
|
||||
api().delete(requests.cocktails.cocktail + cocktail.id)
|
||||
.then(() => {
|
||||
setCocktails(cocktails.filter((r) => r.id !== cocktail.id))
|
||||
setCocktail(emptyCocktail);
|
||||
})
|
||||
.catch(() => createError("Ошибка удаления коктейля"))
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
{/*Загрузка*/}
|
||||
<Loading loading={loading}/>
|
||||
{/*Заголовок*/}
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Коктейли</Typography>
|
||||
</Toolbar>
|
||||
{/*Поиск*/}
|
||||
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 2}}>
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={cocktails}
|
||||
onChange={(e, v) => {
|
||||
if (!v) {
|
||||
setCocktail(emptyCocktail);
|
||||
setSelected(null)
|
||||
} else {
|
||||
setSelected(v.id)
|
||||
}
|
||||
}}
|
||||
isOptionEqualToValue={(selected, value) => selected.id === value.id}
|
||||
getOptionKey={(selected) => selected.id}
|
||||
getOptionLabel={(selected) => selected.name + (selected.hasError ? " (есть ошибка)" : "")}
|
||||
renderInput={(params) => <TextField {...params} label="Поиск"/>}
|
||||
/>
|
||||
</Paper>
|
||||
{/*Рабочая область*/}
|
||||
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 1, pb: 2}}>
|
||||
<Stack>
|
||||
<Box hidden={cocktail.id === null} ml={1} mb={1}>
|
||||
<Button color='error' onClick={() => deleteHandle()}>Удалить коктейль</Button>
|
||||
</Box>
|
||||
{/*Фото*/}
|
||||
<Box ml={1}>
|
||||
<img src={cocktail.image} alt={""} width={300} height={300} loading={'eager'}/>
|
||||
</Box>
|
||||
{/*Редактирование ссылки на фото*/}
|
||||
<Stack direction='row' pr={2} m={1} display='relative'>
|
||||
<TextField sx={{width: '75%'}}
|
||||
label={"Ссылка на фото"} variant='outlined' multiline
|
||||
value={!cocktail.image ? "" : cocktail.image}
|
||||
onChange={(e) => changeCocktailValue("image", e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
component="label"
|
||||
role={undefined}
|
||||
variant="contained"
|
||||
tabIndex={-1}
|
||||
startIcon={<CloudUploadIcon/>}
|
||||
sx={{width: '10%', fontSize: 40, ml: 1, pr: 1}}
|
||||
>
|
||||
<VisuallyHiddenInput
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files[0];
|
||||
let formData = new FormData();
|
||||
formData.append('file', file);
|
||||
api().post(requests.cocktails.savePhoto, formData)
|
||||
.then((r) => changeCocktailValue("image", r.data))
|
||||
.catch(() => getError())
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
</Stack>
|
||||
{/*Название*/}
|
||||
<Box m={1}>
|
||||
<TextField sx={{mr: 1, mb: 2, minWidth: 300}}
|
||||
variant="outlined" label={"Название"}
|
||||
value={cocktail.name}
|
||||
onChange={(e) => changeCocktailValue("name", e.target.value)}/>
|
||||
</Box>
|
||||
{/*Категория, посуда, алкогольность, теги*/}
|
||||
<Box mb={2}>
|
||||
<SelectEdit value={cocktail.category} label={"Категория"} width={300} margin={1}
|
||||
array={category}
|
||||
attributeName={"category"} handler={changeCocktailValue}/>
|
||||
<SelectEdit value={cocktail.glass} label={"Посуда"} width={300} margin={1} array={glass}
|
||||
attributeName={"glass"} handler={changeCocktailValue}/>
|
||||
<SelectEdit value={cocktail.alcoholic} label={"Алкогольность"} width={300} margin={1}
|
||||
array={alcohol}
|
||||
attributeName={"alcoholic"} handler={changeCocktailValue}/>
|
||||
<CheckMarks rows={tags} width={300} name={"Теги"} handleChange={changeCocktailValue}
|
||||
filterValue={cocktail.tags.split(",")} filterName={"tags"}/>
|
||||
</Box>
|
||||
{/*Рецепт*/}
|
||||
<EditCocktailReceipt receipt={cocktail.receipt} handler={changeCocktailValue}/>
|
||||
|
||||
<Box pr={2} ml={1}>
|
||||
<TextField sx={{width: '100%'}}
|
||||
label={"Инструкция"} variant='outlined' multiline
|
||||
value={!cocktail.instructions ? "" : cocktail.instructions}
|
||||
onChange={(e) => changeCocktailValue("instructions", e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box display={'flex'} justifyContent={'flex-end'}>
|
||||
<Button variant='contained' onClick={() => saveHandler()}>Сохранить</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
7
front/src/app/pages/cocktails/MenuPage.js
Normal file
7
front/src/app/pages/cocktails/MenuPage.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import CocktailsPageContent from "./CocktailsPageContent";
|
||||
|
||||
export function MenuPage() {
|
||||
return (
|
||||
<CocktailsPageContent all={false}/>
|
||||
)
|
||||
}
|
||||
169
front/src/app/pages/ingredients/EditIngredientPage.js
Normal file
169
front/src/app/pages/ingredients/EditIngredientPage.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import {Autocomplete, FormControl, FormControlLabel, InputLabel} from "@mui/material";
|
||||
import {api} from "../../../lib/clients/api";
|
||||
import {requests} from "../../../requests";
|
||||
import {useAlert} from "../../../hooks/useAlert";
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import Select from "@mui/material/Select";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {getComparator} from "../../../components/core/getComparator";
|
||||
|
||||
const emptyIngredient = {
|
||||
id: null,
|
||||
name: "",
|
||||
enName: "",
|
||||
have: false,
|
||||
image: null,
|
||||
type: "",
|
||||
alcohol: false,
|
||||
abv: null,
|
||||
description: null
|
||||
}
|
||||
|
||||
export function EditIngredientPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [ingredients, setIngredients] = useState([]);
|
||||
const [types, setTypes] = useState([]);
|
||||
const [ingredient, setIngredient] = useState(emptyIngredient)
|
||||
const {createError, createSuccess} = useAlert();
|
||||
useEffect(() => {
|
||||
api().get(requests.bar.ingredientList)
|
||||
.then((r) => {
|
||||
const arr = r.data.sort(getComparator("asc", "name"));
|
||||
setIngredients(arr)
|
||||
|
||||
const currentId = searchParams.get("id");
|
||||
if (!currentId) {
|
||||
return;
|
||||
}
|
||||
const currentIngredient = arr.find((r) => r.id === (currentId * 1));
|
||||
if (!currentIngredient) {
|
||||
return;
|
||||
}
|
||||
setIngredient(currentIngredient);
|
||||
})
|
||||
.catch(() => createError("Ошибка получения данных"))
|
||||
|
||||
api().get(requests.bar.type)
|
||||
.then((r) => setTypes(r.data.sort(getComparator("asc", "name"))))
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const changeIngredientValue = (name, value) => {
|
||||
setIngredient((prev) => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
}
|
||||
const saveIngredientHandler = () => {
|
||||
api().patch(requests.bar.ingredient, ingredient)
|
||||
.then(() => createSuccess("Ингредиент сохранен"))
|
||||
.catch(() => createError("Ошибка сохранения"))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/*Заголовок*/}
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Ингредиенты</Typography>
|
||||
</Toolbar>
|
||||
{/*Поиск*/}
|
||||
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 2}}>
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={ingredients}
|
||||
|
||||
defaultChecked={emptyIngredient}
|
||||
onChange={(e, v) => {
|
||||
console.log(v);
|
||||
return !v ? setIngredient(emptyIngredient) : setIngredient(v)
|
||||
}}
|
||||
isOptionEqualToValue={(selected, value) => selected.id === value.id}
|
||||
getOptionKey={(selected) => selected.id}
|
||||
getOptionLabel={(selected) => selected.name}
|
||||
renderInput={(params) => <TextField {...params} label="Ингредиенты"/>}
|
||||
/>
|
||||
</Paper>
|
||||
{/*Форма ингредиента*/}
|
||||
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 1, pb: 2}}>
|
||||
<Stack>
|
||||
<Box display={'flex'} justifyContent={'flex-end'} pr={2}>
|
||||
<FormControlLabel control={
|
||||
<Switch
|
||||
checked={ingredient.have}
|
||||
onChange={() => changeIngredientValue("have", !ingredient.have)}
|
||||
/>}
|
||||
label={"Наличие"} labelPlacement={'start'}/>
|
||||
</Box>
|
||||
<Box>
|
||||
<img src={ingredient.image} alt={""} loading={'eager'}/>
|
||||
</Box>
|
||||
<Box m={1}>
|
||||
<TextField sx={{mr: 1, mb: 2, minWidth: 330}}
|
||||
variant="outlined" label={"Название"}
|
||||
value={ingredient.name}
|
||||
onChange={(e) => changeIngredientValue("name", e.target.value)}/>
|
||||
<TextField sx={{mr: 1, mb: 2, minWidth: 330}}
|
||||
label="Английское название" variant="outlined"
|
||||
value={ingredient.enName}
|
||||
onChange={(e) => changeIngredientValue("enName", e.target.value)}/>
|
||||
</Box>
|
||||
|
||||
<Box height={70} mt={1} ml={1}>
|
||||
<FormControlLabel sx={{pt: 1}}
|
||||
control={
|
||||
<Switch
|
||||
checked={ingredient.alcohol}
|
||||
onChange={() => changeIngredientValue("alcohol", !ingredient.alcohol)}
|
||||
/>}
|
||||
label="Алкогольный"/>
|
||||
{ingredient.alcohol && (
|
||||
<TextField sx={{width: 100}}
|
||||
variant='outlined' label='Градус'
|
||||
value={!ingredient.abv ? "" : ingredient.abv}
|
||||
onChange={(e) => changeIngredientValue("abv", e.target.value)}/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box mb={2} ml={1}>
|
||||
<FormControl sx={{width: 330}}>
|
||||
<InputLabel id="select-label">Категория</InputLabel>
|
||||
<Select
|
||||
id="select-label"
|
||||
autoWidth
|
||||
label={"Категория"}
|
||||
value={!ingredient.type ? "" : ingredient.type}
|
||||
onChange={(e) => changeIngredientValue("type", e.target.value)}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{types.map((c) => {
|
||||
return (<MenuItem key={c.id} value={c.name}>{c.name}</MenuItem>)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box pr={2} ml={1}>
|
||||
<TextField sx={{width: '100%'}}
|
||||
label={"Описание"} variant='outlined' multiline
|
||||
value={!ingredient.description ? "" : ingredient.description}/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box display={'flex'} justifyContent={'flex-end'}>
|
||||
<Button variant='contained' onClick={() => saveIngredientHandler()}>Сохранить</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
153
front/src/app/pages/ingredients/IngredientsPage.js
Normal file
153
front/src/app/pages/ingredients/IngredientsPage.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import {Fab, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput, Tabs} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import * as React from "react";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {Loading} from "../../../components/core/Loading";
|
||||
import {requests} from "../../../requests";
|
||||
import {useAlert} from "../../../hooks/useAlert";
|
||||
import {IngredientInfoModal} from "../../../components/Ingredients/IngredientInfoModal";
|
||||
import {api} from "../../../lib/clients/api";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import {a11yProps} from "../../../components/core/tabProps";
|
||||
import {CustomTabPanel} from "../../../components/core/TabPanel";
|
||||
import {IngredientList} from "../../../components/Ingredients/IngredientList";
|
||||
import {blue} from "@mui/material/colors";
|
||||
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import Switch from "@mui/material/Switch";
|
||||
|
||||
export function IngredientsPage() {
|
||||
const [value, setValue] = React.useState(0);
|
||||
const [grouping, setGrouping] = useState(true);
|
||||
const handleChange = (event, newValue) => setValue(newValue);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [findString, setFindString] = useState("");
|
||||
const [ingredients, setIngredients] = useState([]);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [selectedInfo, setSelectedInfo] = useState(null);
|
||||
const {createError} = useAlert();
|
||||
|
||||
useEffect(() => {
|
||||
api().get(requests.bar.ingredientList)
|
||||
.then((r) => {
|
||||
setIngredients(r.data)
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
createError("Ошибка получения списка ингредиентов");
|
||||
setLoading(false);
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const visibleIngredients = useMemo(() => {
|
||||
if (findString.length === 0) {
|
||||
return ingredients;
|
||||
}
|
||||
const reg = new RegExp("(.*?)" + findString + "(.*?)", "i");
|
||||
return ingredients.filter((ingredient) => ingredient.name.match(reg) !== null);
|
||||
}, [findString, ingredients]);
|
||||
const ingredientsToAdd = visibleIngredients.filter((ingredient) => !ingredient.have);
|
||||
const ingredientsInBar = visibleIngredients.filter((ingredient) => ingredient.have);
|
||||
|
||||
const changeHandler = (row, value) => {
|
||||
const newState = ingredients.map((ingredient) => {
|
||||
if (ingredient.id === row.id) {
|
||||
return {
|
||||
...ingredient,
|
||||
have: value
|
||||
}
|
||||
} else {
|
||||
return ingredient;
|
||||
}
|
||||
})
|
||||
const url = `${requests.bar.ingredient}?id=${row.id}`;
|
||||
const request = value ? api().put(url) : api().delete(url);
|
||||
request
|
||||
.then(() => {
|
||||
setIngredients(newState);
|
||||
})
|
||||
.catch(() => {
|
||||
createError("Ошибка изменения ингредиента");
|
||||
});
|
||||
}
|
||||
const handleOpenModal = (i) => {
|
||||
setOpenModal(true);
|
||||
setSelectedInfo(i);
|
||||
}
|
||||
const handleCloseModal = () => {
|
||||
setSelectedInfo(null);
|
||||
setOpenModal(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/*Заголовок*/}
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Ингредиенты бара</Typography>
|
||||
</Toolbar>
|
||||
{/*Поиск*/}
|
||||
<Paper elevation={6} sx={{my: 2}}>
|
||||
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
|
||||
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||
<OutlinedInput
|
||||
onChange={(e) => setFindString(e.target.value)}
|
||||
label="With normal TextField"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconButton edge="end">
|
||||
<SearchIcon/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControlLabel sx={{ml: '2px'}}
|
||||
control={<Switch defaultChecked/>}
|
||||
onClick={() => setGrouping(!grouping)}
|
||||
label="Группировать"
|
||||
labelPlacement="end"/>
|
||||
</Paper>
|
||||
{/*Рабочее поле ингредиентов*/}
|
||||
<Box>
|
||||
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
|
||||
<Tab label="В баре" {...a11yProps(0)} />
|
||||
<Tab label="Список" {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box>
|
||||
<CustomTabPanel value={value} index={0}>
|
||||
<IngredientList rows={ingredientsInBar} value={false} changeHandler={changeHandler}
|
||||
infoHandler={handleOpenModal} grouping={grouping}/>
|
||||
</CustomTabPanel>
|
||||
<CustomTabPanel value={value} index={1}>
|
||||
<IngredientList rows={ingredientsToAdd} value={true} changeHandler={changeHandler}
|
||||
infoHandler={handleOpenModal} grouping={grouping}/>
|
||||
</CustomTabPanel>
|
||||
</Box>
|
||||
<Fab sx={{
|
||||
alpha: '30%',
|
||||
position: 'sticky',
|
||||
bottom: '16px',
|
||||
color: 'common.white',
|
||||
bgcolor: blue[600],
|
||||
'&:hover': {
|
||||
bgcolor: blue[600],
|
||||
},
|
||||
}}
|
||||
onClick={() => window.window.scrollTo(0, 0)}
|
||||
aria-label='Expand'
|
||||
color='inherit'>
|
||||
<UpIcon/>
|
||||
</Fab>
|
||||
{/*Загрузчик*/}
|
||||
<Loading loading={loading}/>
|
||||
{/*Модальное окно информации об ингредиенте*/}
|
||||
<IngredientInfoModal ingredient={selectedInfo} open={openModal} closeHandler={handleCloseModal}/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
38
front/src/app/pages/notFound/NotFoundPage.js
Normal file
38
front/src/app/pages/notFound/NotFoundPage.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import {paths} from "../../../path";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<Box component="main" sx={{ alignItems: 'center', display: 'flex', justifyContent: 'center', minHeight: '100%' }}>
|
||||
<Stack spacing={3} sx={{ alignItems: 'center', maxWidth: 'md' }}>
|
||||
<Box>
|
||||
<Box
|
||||
component="img"
|
||||
alt="Under development"
|
||||
src="/assets/error-404.png"
|
||||
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="h3" sx={{ textAlign: 'center' }}>
|
||||
404: Страница не найдена или недоступна
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body1" sx={{ textAlign: 'center' }}>
|
||||
Вы либо выбрали какой-то сомнительный маршрут, либо попали сюда по ошибке. Что бы это ни было, попробуйте воспользоваться навигацией
|
||||
</Typography>
|
||||
<Button
|
||||
// component={'a'}
|
||||
href={paths.home}
|
||||
startIcon={<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />}
|
||||
variant="contained"
|
||||
>
|
||||
На домашнюю страницу
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
7
front/src/app/pages/queue/MyQueuePage.js
Normal file
7
front/src/app/pages/queue/MyQueuePage.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import {QueueContent} from "./QueueContent";
|
||||
|
||||
export function MyQueuePage() {
|
||||
return (
|
||||
<QueueContent my={true}/>
|
||||
)
|
||||
}
|
||||
82
front/src/app/pages/queue/QueueContent.js
Normal file
82
front/src/app/pages/queue/QueueContent.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useAlert} from "../../../hooks/useAlert";
|
||||
import * as React from "react";
|
||||
import {api} from "../../../lib/clients/api";
|
||||
import {requests} from "../../../requests";
|
||||
import {createHeadCell} from "../../../components/orders/createHeadCelll";
|
||||
import {Loading} from "../../../components/core/Loading";
|
||||
import OrderModal from "../../../components/orders/OrderModal";
|
||||
import EnhancedTable from "../../../components/orders/EnhancedTable";
|
||||
|
||||
export function QueueContent({my}) {
|
||||
const [load, setLoad] = useState(false);
|
||||
const [orders, setOrders] = useState([]);
|
||||
const {createSuccess, createError} = useAlert();
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoad(false);
|
||||
const url = my ? requests.bar.myOrders : requests.bar.order;
|
||||
api().get(url)
|
||||
.then(r => {
|
||||
setOrders(r.data);
|
||||
setLoad(true);
|
||||
})
|
||||
.catch(() => {
|
||||
createError("Ошибка при получении заказов");
|
||||
setLoad(true)
|
||||
})
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const sliced = useMemo(() => orders.sort((a, b) => b.id - a.id), [orders])
|
||||
|
||||
const cells = [
|
||||
createHeadCell('id', true, true, 'Номер заказа', "20px"),
|
||||
createHeadCell('cocktail.name', true, false, 'Коктейль', "40px"),
|
||||
createHeadCell('visitor.name', true, false, 'Клиент', "40px"),
|
||||
createHeadCell('status', true, true, 'Статус', "30px"),
|
||||
];
|
||||
|
||||
const changeOrderHandle = (row, status) => {
|
||||
let url = requests.bar.order + "?id=" + row.id;
|
||||
let isCancel = status === "CANCEL";
|
||||
let request = isCancel ? api().delete(url) : api().put(url);
|
||||
request
|
||||
.then(() => {
|
||||
createSuccess(isCancel ? "Заказ отменен" : "Заказ готов");
|
||||
let newArr = orders.filter((order) => {
|
||||
if (order.id !== row.id) {
|
||||
row.status = isCancel ? "CANCEL" : "DONE";
|
||||
return row;
|
||||
}
|
||||
return order;
|
||||
})
|
||||
setOrders(newArr);
|
||||
setSelected(null);
|
||||
setOpenModal(false);
|
||||
})
|
||||
.catch(() => createError("Ошибка изменения заказа"))
|
||||
}
|
||||
const handleSelect = (row) => {
|
||||
setSelected(row);
|
||||
setOpenModal(true);
|
||||
}
|
||||
const handleCloseModal = () => {
|
||||
setOpenModal(false);
|
||||
setSelected(null);
|
||||
};
|
||||
|
||||
const filterValues = !my ? ["DONE", "CANCEL"] : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Loading loading={!load}/>
|
||||
<OrderModal row={selected} handleClose={handleCloseModal} open={openModal}
|
||||
handleChange={changeOrderHandle} my={my}/>
|
||||
<EnhancedTable name={"Заказы"} rows={sliced} cells={cells} handleSelect={handleSelect} filterEqual={false}
|
||||
filterField={["status"]} filterValue={filterValues}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
10
front/src/app/pages/queue/QueuePage.js
Normal file
10
front/src/app/pages/queue/QueuePage.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as React from "react";
|
||||
import {QueueContent} from "./QueueContent";
|
||||
|
||||
const QueuePage = () => {
|
||||
return (
|
||||
<QueueContent my={false}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueuePage;
|
||||
Reference in New Issue
Block a user