Initial commit
This commit is contained in:
37
front/src/components/Ingredients/IngredientCard.js
Normal file
37
front/src/components/Ingredients/IngredientCard.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import {Card} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddBoxRoundedIcon from '@mui/icons-material/AddBoxRounded';
|
||||
import InfoRoundedIcon from '@mui/icons-material/InfoRounded';
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {paths} from "../../path";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
export function IngredientCard({row, value, infoHandler, changeHandler}) {
|
||||
return (
|
||||
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1}}>
|
||||
<Stack direction='row' justifyContent='start' alignItems='center'>
|
||||
<Box sx={{width: '100px', height: '100px'}}>
|
||||
<img src={row.image} loading='eager' height={'100px'} width={'100px'} alt={row.id}/>
|
||||
</Box>
|
||||
<Box sx={{width: 'calc(90% - 100px)', pr: 2}}>{row.name}</Box>
|
||||
<Stack direction='row'>
|
||||
<Box mr={1} pt={'3px'}>{!row.alcohol ? "" : `${row.abv}%`}</Box>
|
||||
<Stack sx={{width: '5%'}} spacing={1} justifyContent='flex-start'>
|
||||
<IconButton size='small' onClick={() => changeHandler(row, value)}>
|
||||
{value ? <AddBoxRoundedIcon/> : <DeleteIcon/>}
|
||||
</IconButton>
|
||||
<IconButton size='small' onClick={() => infoHandler(row)}>
|
||||
<InfoRoundedIcon/>
|
||||
</IconButton>
|
||||
<IconButton size='small' href={`${paths.bar.ingredientEdit}?id=${row.id}`}>
|
||||
<EditIcon/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
37
front/src/components/Ingredients/IngredientInfoModal.js
Normal file
37
front/src/components/Ingredients/IngredientInfoModal.js
Normal file
@@ -0,0 +1,37 @@
|
||||
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 Button from "@mui/material/Button";
|
||||
import * as React from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export function IngredientInfoModal({ingredient, open, closeHandler}) {
|
||||
if (!ingredient) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dialog fullWidth={true} maxWidth="350px" open={open} onClose={closeHandler}
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
margin: '8px',
|
||||
},
|
||||
'& .MuiPaper-root': {
|
||||
width: 'calc(100% - 16px)',
|
||||
}
|
||||
}}>
|
||||
<DialogTitle>{ingredient.name}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{justifyContent: "center"}}>
|
||||
<img src={ingredient.image} alt={ingredient.name} loading={"eager"} width={"300"}/>
|
||||
{ingredient.alcohol && (<Typography>{`Крепость ${ingredient.abv}`}</Typography>)}
|
||||
<Typography>{ingredient.description}</Typography>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeHandler}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
75
front/src/components/Ingredients/IngredientList.js
Normal file
75
front/src/components/Ingredients/IngredientList.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import {IngredientCard} from "./IngredientCard";
|
||||
import {useMemo, useState} from "react";
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {getComparator} from "../core/getComparator";
|
||||
import {groupByForLoop} from "../core/groupByForLoop";
|
||||
|
||||
export function IngredientList({rows, value, infoHandler, changeHandler, grouping}) {
|
||||
const [size, setSize] = useState(10);
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= (document.documentElement.scrollHeight - 100)) {
|
||||
if (!grouping) {
|
||||
setSize(size + 10)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const visibleRows = useMemo(() => {
|
||||
let res = [];
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!grouping) {
|
||||
return rows
|
||||
.sort(getComparator("asc", "name"))
|
||||
.slice(0, size)
|
||||
.map((row) => {
|
||||
return (
|
||||
<IngredientCard row={row} key={row.id} value={value}
|
||||
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const group = groupByForLoop(rows, "type")
|
||||
if (!group || group.size === 0) {
|
||||
return null;
|
||||
}
|
||||
const keys = Array.from(group.keys());
|
||||
keys.sort(getComparator("asc"))
|
||||
.forEach((key) => {
|
||||
const list = group.get(key)
|
||||
res.push(
|
||||
<Accordion key={key}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon/>}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
>
|
||||
<Typography component="span">{key !== "null" ? key : "Без категории"}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{list.sort(getComparator("asc", "name"))
|
||||
.map((row) => {
|
||||
return (
|
||||
<IngredientCard row={row} key={row.id} value={value}
|
||||
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||
)
|
||||
})}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
})
|
||||
return res;
|
||||
// eslint-disable-next-line
|
||||
}, [size, rows])
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
{visibleRows}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
48
front/src/components/auth/guest-guard.js
Normal file
48
front/src/components/auth/guest-guard.js
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import {useAuth} from "../../hooks/useAuth";
|
||||
import {logger} from "../../lib/DefaultLogger";
|
||||
import {paths} from "../../path";
|
||||
|
||||
export function GuestGuard({ children }) {
|
||||
const { auth, error, isLoading } = useAuth();
|
||||
const [isChecking, setIsChecking] = React.useState(true);
|
||||
|
||||
const checkPermissions = async () => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setIsChecking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
logger.debug('[GuestGuard]: User is logged in, redirecting to dashboard');
|
||||
window.location.replace(paths.dashboard.overview);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecking(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
checkPermissions().catch(() => {
|
||||
// noop
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
|
||||
}, [auth, error, isLoading]);
|
||||
|
||||
if (isChecking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert color="error">{error}</Alert>;
|
||||
}
|
||||
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
}
|
||||
135
front/src/components/auth/sign-in-form.js
Normal file
135
front/src/components/auth/sign-in-form.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as React from 'react';
|
||||
import {useState} from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {paths} from "../../path";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Button from "@mui/material/Button";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Box from "@mui/material/Box";
|
||||
import {red} from "@mui/material/colors";
|
||||
import {requests} from "../../requests";
|
||||
import {useAuth} from "../../hooks/useAuth";
|
||||
import {api} from "../../lib/clients/api";
|
||||
|
||||
const emptyRequest = {
|
||||
byLogin: false,
|
||||
code: "",
|
||||
login: "",
|
||||
password: ""
|
||||
}
|
||||
|
||||
export function SignInForm() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [request, setRequest] = useState(emptyRequest);
|
||||
const [pass, setPass] = useState(false)
|
||||
const {checkSession} = useAuth();
|
||||
|
||||
const buttonSx = {
|
||||
minWidth: "300px",
|
||||
...(error && {
|
||||
bgcolor: red[500],
|
||||
'&:hover': {
|
||||
bgcolor: red[700],
|
||||
},
|
||||
}),
|
||||
};
|
||||
const handleButtonClick = async () => {
|
||||
setLoading(true);
|
||||
const response = await api().post(requests.auth.login, request);
|
||||
|
||||
if (response.data.error) {
|
||||
setError(response.data.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("token", response.data.token);
|
||||
|
||||
await checkSession?.();
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
const renderByCode = () => {
|
||||
return (
|
||||
<Stack direction='row' mt={1}>
|
||||
<IconButton href={paths.auth.bot} target="_blank" color='primary'>
|
||||
<TelegramIcon/>
|
||||
</IconButton>
|
||||
|
||||
<TextField value={request.code}
|
||||
onChange={(e) => setRequest((prevState) => ({
|
||||
...prevState,
|
||||
code: e.target.value
|
||||
}))}
|
||||
sx={{minWidth: 300}} id="outlined-basic" label="Код подтверждения"
|
||||
variant="outlined"/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
const renderByLogin = () => {
|
||||
return (
|
||||
<Stack mt={1} spacing={2}>
|
||||
<TextField value={request.login}
|
||||
onChange={(e) => setRequest(prevState => ({
|
||||
...prevState,
|
||||
login: e.target.value
|
||||
}))}
|
||||
sx={{minWidth: 300}} id="loginField" label="Логин"
|
||||
variant="outlined"/>
|
||||
<TextField value={request.password}
|
||||
onChange={(e) => setRequest((prevState) => ({
|
||||
...prevState,
|
||||
password: e.target.value
|
||||
}))}
|
||||
sx={{minWidth: 300}} id="passwordField" label="Пароль" type="password"
|
||||
autoComplete="current-password" variant="outlined"/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4} sx={{marginBottom: '85%', marginTop: '45%'}}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h4">Авторизация</Typography>
|
||||
<Typography variant='body1' component="a" href='#'
|
||||
onClick={() => {
|
||||
setRequest((prevState) => ({
|
||||
...prevState,
|
||||
byLogin: !pass
|
||||
}))
|
||||
setPass(!pass)
|
||||
}}>
|
||||
{pass ? "Вход по телеграмм-коду" : "Вход по логину и паролю"}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{pass ? "Введите логин и пароль"
|
||||
: "Для входа нужно всего лишь сказать об этом моему Telegram-боту, перейди по ссылке и набери \n/start"}
|
||||
</Typography>
|
||||
{pass ? renderByLogin() : renderByCode()}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', ml: '40px', mt: 1}}>
|
||||
<Stack>
|
||||
{error && (
|
||||
<Typography mb={1} color={'error'}>{error}</Typography>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={buttonSx}
|
||||
disabled={loading}
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress
|
||||
size={25}
|
||||
/>
|
||||
) : "Войти"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
40
front/src/components/bar/BarItem.js
Normal file
40
front/src/components/bar/BarItem.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {Card} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
const role = (myRole) => {
|
||||
switch (myRole) {
|
||||
case "ADMIN":
|
||||
return "Администратор";
|
||||
case "ADMIN_NOT_BARMEN":
|
||||
return "Управляющий";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function BarItem({row, changeHandler, all, enterExist}) {
|
||||
return (
|
||||
<Card sx={{mb: 1, height: '100px', display: 'relative', p: 1}}>
|
||||
<Stack direction='row' justifyContent='start' alignItems='start'>
|
||||
<Box sx={{width: '75%', pr: 2}}>
|
||||
<Typography variant='h6'>{row.name}</Typography>
|
||||
<Typography>{role(row.myRole)}</Typography>
|
||||
</Box>
|
||||
<Stack sx={{width: '25%'}} spacing={1} justifyContent='flex-end' display='flex'>
|
||||
<Typography color={row.open ? 'green' : 'red'}>{row.open ? "Бар открыт" : "Бар закрыт"}</Typography>
|
||||
<Button variant='contained'
|
||||
color={row.enter ? 'error' : 'success'}
|
||||
onClick={() => changeHandler(row, !row.enter)}
|
||||
disabled={!row.enter && enterExist}
|
||||
>
|
||||
{all ? "Добавить" : row.enter ? "Выйти" : "Войти"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
70
front/src/components/bar/BarList.js
Normal file
70
front/src/components/bar/BarList.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {api} from "../../lib/clients/api";
|
||||
import {requests} from "../../requests";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import {BarItem} from "./BarItem";
|
||||
import {Loading} from "../core/Loading";
|
||||
import * as React from "react";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
|
||||
export function BarList({all}) {
|
||||
const {getError, createError} = useAlert();
|
||||
const {refresh} = useUser();
|
||||
const [bars, setBars] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
api().get(`${requests.bar.list}?my=${!all}`)
|
||||
.then((r) => {
|
||||
setBars(r.data)
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => getError())
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const enterExist = useMemo(() => bars.find((b) => b.enter), [bars])
|
||||
const changeHandler = (row, value) => {
|
||||
let request;
|
||||
let newState;
|
||||
if (!all) {
|
||||
if (value && enterExist) {
|
||||
//todo: добавить переключение
|
||||
createError("Нельзя войти более чем в один бар одновременно")
|
||||
return;
|
||||
}
|
||||
request = api().patch(`${requests.bar.enter}${row.id}&value=${value}`);
|
||||
newState = bars.map((b) => {
|
||||
if (b.id !== row.id) {
|
||||
return b;
|
||||
}
|
||||
return {
|
||||
...b,
|
||||
enter: value
|
||||
}
|
||||
})
|
||||
} else {
|
||||
request = api().post(requests.bar.addToMyList, row);
|
||||
newState = bars.filter((b) => b.id !== row.id);
|
||||
}
|
||||
request.then(() => {
|
||||
setBars(newState)
|
||||
refresh();
|
||||
}).catch(() => getError())
|
||||
}
|
||||
return (
|
||||
<Box mt={2}>
|
||||
{
|
||||
bars.map((row) => {
|
||||
return (
|
||||
<BarItem key={row.id} row={row} changeHandler={changeHandler}
|
||||
all={all} enterExist={enterExist}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
{/*Загрузчик*/}
|
||||
<Loading loading={loading}/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
68
front/src/components/cocktails/CheckMarks.js
Normal file
68
front/src/components/cocktails/CheckMarks.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Select from '@mui/material/Select';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function CheckMarks({rows, name, filterValue, handleChange, filterName, width, nonMulti, nullValue, identity}) {
|
||||
const realValue = !nonMulti ? filterValue.filter((v) => v.length > 0) : filterValue;
|
||||
return (
|
||||
<div>
|
||||
<FormControl sx={{m: 1, width: !width ? 300 : width}}>
|
||||
<InputLabel>{name}</InputLabel>
|
||||
<Select
|
||||
multiple={!nonMulti}
|
||||
value={realValue}
|
||||
onChange={(e) => handleChange(filterName, e.target.value)}
|
||||
input={<OutlinedInput label={name}/>}
|
||||
renderValue={(selected) => !nonMulti ? selected.join(", ") : selected}
|
||||
MenuProps={MenuProps}
|
||||
defaultChecked={nonMulti && rows[0]}
|
||||
variant="filled">
|
||||
{(nonMulti && nullValue) && (
|
||||
<MenuItem value={""}>
|
||||
<em>Не выбрано</em>
|
||||
</MenuItem>
|
||||
)}
|
||||
{rows.map((value) => {
|
||||
if(identity) {
|
||||
return (
|
||||
<MenuItem key={"menuItemIn" + value} value={value}>
|
||||
{!nonMulti && (
|
||||
<Checkbox
|
||||
checked={realValue.includes(value)}/>
|
||||
)}
|
||||
<ListItemText primary={value}/>
|
||||
</MenuItem>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<MenuItem key={value.id} value={value.name}>
|
||||
{!nonMulti && (
|
||||
<Checkbox
|
||||
checked={realValue.includes(value.name)}/>
|
||||
)}
|
||||
<ListItemText primary={value.name}/>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
113
front/src/components/cocktails/Cocktail.js
Normal file
113
front/src/components/cocktails/Cocktail.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import {CardActions, CardContent, CardMedia, Rating} from "@mui/material";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Button from "@mui/material/Button";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {requests} from "../../requests";
|
||||
import {CocktailItemStyled} from "./CocktailItemStyled";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteTwoTone';
|
||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import {api} from "../../lib/clients/api";
|
||||
import Box from "@mui/material/Box";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
|
||||
function renderFavouriteBadge(handleFavourite, row) {
|
||||
const childIcon = row.rating.favourite ? <FavoriteIcon color='error'/> : <FavoriteBorderIcon color={'warning'}/>;
|
||||
return (
|
||||
<IconButton sx={{position: 'absolute', top: "15px", right: "15px"}} onClick={() => handleFavourite(row)}>
|
||||
{childIcon}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
function renderRating(handleChangeRating, row) {
|
||||
return (
|
||||
<Rating
|
||||
sx={{position: 'absolute', top: '310px', right: '85px'}}
|
||||
name="simple-controlled"
|
||||
size="large"
|
||||
value={row.rating.rating}
|
||||
onChange={(event, newValue) => handleChangeRating(row, newValue)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Cocktail({row, handleFavourite, handleChangeRating, handleSelect, editMenuBlock}) {
|
||||
const {createAlert, createError} = useAlert();
|
||||
const {session, user} = useUser();
|
||||
|
||||
function pay(cocktailId) {
|
||||
api().post(`${requests.bar.pay}cocktail=${cocktailId}`)
|
||||
.then(() => createAlert("Ожидайте свой заказ", "success"))
|
||||
.catch(() => createError("Ошибка во время создания заказа"))
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item sx={{pr: 2}}>
|
||||
<CocktailItemStyled>
|
||||
<Box sx={{
|
||||
p: '4px 4px',
|
||||
m: 1,
|
||||
width: '320px',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<CardMedia
|
||||
sx={{
|
||||
loading: "eager",
|
||||
borderRadius: 2
|
||||
}}
|
||||
onClick={() => handleSelect(row)}
|
||||
component="img"
|
||||
alt={row.name}
|
||||
height="300"
|
||||
|
||||
image={`${row.image}/preview`}
|
||||
/>
|
||||
{renderFavouriteBadge(handleFavourite, row)}
|
||||
{renderRating(handleChangeRating, row)}
|
||||
<CardContent sx={{pb: '4px', pl: 2}}>
|
||||
<Typography variant="h5" minHeight={'50px'} mt={2}>{row.name} </Typography>
|
||||
<List sx={{py: '0px'}}>
|
||||
{row.hasError && (
|
||||
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||
<ListItemText color={'red'}>Имеет ошибку в рецепте или ингредиентах</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||
<ListItemText>{"Категория: " + row.category}</ListItemText>
|
||||
</ListItem>
|
||||
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||
<ListItemText>{"Алкоголь: " + row.alcoholic}</ListItemText>
|
||||
</ListItem>
|
||||
{row.volume !== null && (
|
||||
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||
<ListItemText>{"Крепость: ≈" + row.volume}</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||
<ListItemText>{"Подача: " + row.glass}</ListItemText>
|
||||
</ListItem>
|
||||
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||
<ListItemText>{"Состав: " + row.components}</ListItemText>
|
||||
</ListItem>
|
||||
{(row.tags && row.tags.length > 0) && (
|
||||
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||
<ListItemText>{"Теги: " + row.tags.replaceAll(',', ', ')}</ListItemText>
|
||||
</ListItem>)}
|
||||
</List>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
{(row.isAllowed && session.isActive && user.invited) &&
|
||||
<Button variant="contained" onClick={() => pay(row.id)}>Заказать</Button>
|
||||
}
|
||||
{editMenuBlock(row)}
|
||||
</CardActions>
|
||||
</Box>
|
||||
</CocktailItemStyled>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
180
front/src/components/cocktails/CocktailInfoModal.js
Normal file
180
front/src/components/cocktails/CocktailInfoModal.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import * as React from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {CardMedia} from "@mui/material";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Box from "@mui/material/Box";
|
||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import {IngredientInfoModal} from "../Ingredients/IngredientInfoModal";
|
||||
import {api} from "../../lib/clients/api";
|
||||
import {requests} from "../../requests";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import {paths} from "../../path";
|
||||
import {Loading} from "../core/Loading";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
|
||||
export function CocktailInfoModal({open, row, closeHandler}) {
|
||||
const {user} = useUser();
|
||||
const {getError, createError, createSuccess} = useAlert();
|
||||
const [cocktail, setCocktail] = useState(null)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedIngredient, setSelectedIngredient] = useState(null);
|
||||
const [openIngredientModal, setOpenIngredientModal] = useState(false)
|
||||
const closeIngredientHandler = () => {
|
||||
setOpenIngredientModal(false);
|
||||
setSelectedIngredient(null);
|
||||
}
|
||||
const openIngredientModalHandler = (id) => {
|
||||
api().get(`${requests.bar.ingredient}?id=${id}`)
|
||||
.then((r) => {
|
||||
setSelectedIngredient(r.data)
|
||||
setOpenIngredientModal(true);
|
||||
}).catch(() => createError("Ошибка получения информации об ингредиенте"))
|
||||
}
|
||||
const selectIngredientHandler = (ingredient) => {
|
||||
const url = `${requests.bar.ingredient}?id=${ingredient.id}`;
|
||||
const request = ingredient.isHave ? api().delete(url) : api().put(url);
|
||||
const value = !ingredient.isHave;
|
||||
request.then(() => {
|
||||
const newReceipts = cocktail.receipt.map((r) => {
|
||||
if (r.ingredient.id !== ingredient.id) {
|
||||
return r;
|
||||
}
|
||||
return {
|
||||
...r,
|
||||
ingredient: {
|
||||
...ingredient,
|
||||
isHave: value
|
||||
}
|
||||
}
|
||||
})
|
||||
setCocktail({
|
||||
...cocktail,
|
||||
receipt: newReceipts
|
||||
})
|
||||
createSuccess("Сохранено")
|
||||
}).catch(() => createError("Ошибка сохранения"))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
if (!row) {
|
||||
setLoading(false)
|
||||
return;
|
||||
}
|
||||
api().get(requests.cocktails.modal + row)
|
||||
.then((r) => {
|
||||
setCocktail(r.data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => {
|
||||
getError();
|
||||
setLoading(false)
|
||||
closeHandler();
|
||||
})
|
||||
}, [row]);
|
||||
|
||||
if (!row || !cocktail) {
|
||||
return null;
|
||||
}
|
||||
let alko = 0;
|
||||
let volume = 0;
|
||||
return (
|
||||
<Dialog fullWidth={true}
|
||||
open={open} onClose={closeHandler}
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
margin: '8px',
|
||||
},
|
||||
'& .MuiPaper-root': {
|
||||
width: 'calc(100% - 16px)',
|
||||
}
|
||||
}}>
|
||||
<IngredientInfoModal ingredient={selectedIngredient} open={openIngredientModal}
|
||||
closeHandler={closeIngredientHandler}/>
|
||||
<Loading loading={loading}/>
|
||||
<DialogTitle>
|
||||
<Stack direction='row' justifyContent={'space-between'}>
|
||||
<Box>{cocktail.name}</Box>
|
||||
|
||||
{cocktail.rating.rating > 0 &&
|
||||
(
|
||||
<Stack ml={3} direction='row'>
|
||||
{`${cocktail.rating.rating}/5`}
|
||||
<StarBorderIcon sx={{pb: "2px"}}/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<CardMedia
|
||||
image={`${cocktail.image}/preview`}
|
||||
sx={{
|
||||
loading: "eager",
|
||||
borderRadius: 2
|
||||
}}
|
||||
component="img"
|
||||
alt={cocktail.name}
|
||||
height="300"
|
||||
/>
|
||||
<Box mt={1}>
|
||||
<Typography>Ингредиенты:</Typography>
|
||||
<Paper sx={{p: 1}} elevation={3}>
|
||||
<Stack>
|
||||
{cocktail.receipt.map((r) => {
|
||||
const hasError = r.count === null || r.unit === null;
|
||||
const measure = hasError ? r.measure : (r.count + " " + r.unit.name)
|
||||
if(alko !== null && volume !== null) {
|
||||
console.log(r)
|
||||
}
|
||||
return (
|
||||
<Stack key={r.ingredient.id} direction='row' justifyContent={'space-between'}
|
||||
mt={1}>
|
||||
<Stack direction='row'>
|
||||
{user.role !== "USER" && (
|
||||
<IconButton size="small" sx={{pb: "2px"}}
|
||||
onClick={() => selectIngredientHandler(r.ingredient)}>
|
||||
{r.ingredient.isHave
|
||||
? (<DeleteIcon fontSize="small"/>)
|
||||
: (<ShoppingCartIcon fontSize="small"/>)
|
||||
}
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography
|
||||
onClick={() => openIngredientModalHandler(r.ingredient.id)}>{r.ingredient.name}</Typography>
|
||||
</Stack>
|
||||
<Typography color={hasError && 'red'}>{measure}</Typography>
|
||||
</Stack>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography mt={2}>Инструкция:</Typography>
|
||||
<Paper sx={{p: 1}} elevation={3}>
|
||||
<Box>
|
||||
{cocktail.instructions}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{user.role.includes("ADMIN") && (
|
||||
<Button href={`${paths.bar.cocktailEdit}?id=${cocktail.id}`}>Редактировать</Button>
|
||||
)}
|
||||
<Button onClick={closeHandler}>Закрыть</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
12
front/src/components/cocktails/CocktailItemStyled.js
Normal file
12
front/src/components/cocktails/CocktailItemStyled.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {styled} from "@mui/material/styles";
|
||||
import {Card} from "@mui/material";
|
||||
|
||||
export const CocktailItemStyled = styled(Card)(({theme}) => ({
|
||||
backgroundColor: '#fff',
|
||||
...theme.typography.body2,
|
||||
padding: theme.spacing(1),
|
||||
color: theme.palette.text.secondary,
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: '#1A2027',
|
||||
})
|
||||
}));
|
||||
44
front/src/components/cocktails/CocktailListCard.js
Normal file
44
front/src/components/cocktails/CocktailListCard.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import {Card, FormControlLabel} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import InfoRoundedIcon from "@mui/icons-material/InfoRounded";
|
||||
import {paths} from "../../path";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import React from "react";
|
||||
import Switch from "@mui/material/Switch";
|
||||
|
||||
export function CocktailListCard({row, changeHandler, infoHandler}) {
|
||||
return (
|
||||
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1, borderRadius: '15px'}}>
|
||||
<Stack direction='row' justifyContent='start' alignItems='start'>
|
||||
<Box sx={{width: '100px', height: '100px', ml: 1}}>
|
||||
<img src={row.image} loading='eager' height={'100px'} width={'100px'} alt={row.id}
|
||||
style={{borderRadius: '20%'}}/>
|
||||
</Box>
|
||||
<Stack sx={{width: 'calc(95% - 100px)', pr: 2, ml: 1}}>
|
||||
<Box>{row.name}</Box>
|
||||
<FormControlLabel sx={{mt: 5, pr: 2}}
|
||||
onClick={() => changeHandler(row, !row.inMenu)}
|
||||
value="bottom"
|
||||
control={
|
||||
<Switch color="primary" checked={row.inMenu}/>
|
||||
}
|
||||
label="В меню"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction='row'>
|
||||
<Stack sx={{width: '5%', mt: 2}} spacing={1} justifyContent='flex-start'>
|
||||
<IconButton size='small' onClick={() => infoHandler(row)}>
|
||||
<InfoRoundedIcon/>
|
||||
</IconButton>
|
||||
<IconButton size='small' href={`${paths.bar.cocktailEdit}?id=${row.id}`}>
|
||||
<EditIcon/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
76
front/src/components/cocktails/CocktailsList.js
Normal file
76
front/src/components/cocktails/CocktailsList.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import {useMemo, useState} from "react";
|
||||
import {getComparator} from "../core/getComparator";
|
||||
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import {CocktailListCard} from "./CocktailListCard";
|
||||
import {groupByForLoop} from "../core/groupByForLoop";
|
||||
|
||||
export function CocktailsList({rows, grouping, changeHandler, infoHandler}) {
|
||||
const [size, setSize] = useState(20);
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= (document.documentElement.scrollHeight - 100)) {
|
||||
if (!grouping) {
|
||||
setSize(size + 10)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const visibleRows = useMemo(() => {
|
||||
let res = [];
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!grouping) {
|
||||
return rows
|
||||
.sort(getComparator("asc", "name"))
|
||||
.slice(0, size)
|
||||
.map((row) => {
|
||||
return (
|
||||
<CocktailListCard row={row} key={row.id}
|
||||
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const group = groupByForLoop(rows, "category")
|
||||
if (!group || group.size === 0) {
|
||||
return null;
|
||||
}
|
||||
Array.from(group.keys())
|
||||
.sort(getComparator())
|
||||
.map((key) => {
|
||||
const list = group.get(key);
|
||||
res.push(
|
||||
<Accordion key={key} sx={{borderRadius: '5px', mb: 1}}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon/>}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
>
|
||||
<Typography component="span">{key}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{p: 1}}>
|
||||
{list.sort(getComparator("asc", "name"))
|
||||
.map((row) => {
|
||||
return (
|
||||
<CocktailListCard row={row} key={row.id}
|
||||
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||
)
|
||||
})}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
})
|
||||
return res;
|
||||
// eslint-disable-next-line
|
||||
}, [size, rows])
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
{visibleRows}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
167
front/src/components/cocktails/EditCocktailReceipt.js
Normal file
167
front/src/components/cocktails/EditCocktailReceipt.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import * as React from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import {api} from "../../lib/clients/api";
|
||||
import {requests} from "../../requests";
|
||||
import {getComparator} from "../core/getComparator";
|
||||
import {Card} from "@mui/material";
|
||||
import {SelectEdit} from "./SelectEdit";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
|
||||
export function EditCocktailReceipt({receipt, handler}) {
|
||||
const {createError} = useAlert()
|
||||
const [ingredients, setIngredients] = useState([]);
|
||||
const [units, setUnits] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
api().get(requests.bar.ingredientList)
|
||||
.then((r) => setIngredients(r.data.sort(getComparator("asc", "name"))))
|
||||
.catch(() => createError("Ошибка получения списка ингредиентов"))
|
||||
|
||||
api().get(requests.bar.unit)
|
||||
.then((r) => setUnits(r.data.sort(getComparator("asc", "name"))))
|
||||
.catch(() => createError("Ошибка получения единиц измерения"))
|
||||
}, []);
|
||||
|
||||
const selectHandler = (name, value) => {
|
||||
const ing = ingredients.find((i) => i.name === value)
|
||||
const newState = receipt.map((r, i) => {
|
||||
if (i !== name) {
|
||||
return r;
|
||||
}
|
||||
return {
|
||||
id: r.id,
|
||||
ingredient: {
|
||||
id: ing.id,
|
||||
isHave: ing.have,
|
||||
name: ing.name
|
||||
},
|
||||
measure: r.measure
|
||||
}
|
||||
})
|
||||
handler("receipt", newState);
|
||||
checkAllowed(newState);
|
||||
}
|
||||
const unitHandler = (name, value) => {
|
||||
const ing = units.find((i) => i.name === value)
|
||||
const newState = receipt.map((r, i) => {
|
||||
if (i !== name) {
|
||||
return r;
|
||||
}
|
||||
return {
|
||||
id: r.id,
|
||||
ingredient: r.ingredient,
|
||||
unit: ing,
|
||||
count: r.count,
|
||||
measure: r.measure
|
||||
}
|
||||
})
|
||||
handler("receipt", newState);
|
||||
checkAllowed(newState);
|
||||
}
|
||||
const removeHandler = (index) => {
|
||||
const arr = receipt.filter((r, i) => i !== index)
|
||||
handler("receipt", arr)
|
||||
checkAllowed(arr)
|
||||
}
|
||||
const addHandler = () => {
|
||||
const oldState = receipt;
|
||||
oldState.push({
|
||||
id: null,
|
||||
ingredient: {
|
||||
id: null,
|
||||
isHave: false,
|
||||
name: ""
|
||||
},
|
||||
measure: ""
|
||||
});
|
||||
handler("receipt", oldState);
|
||||
checkAllowed(oldState);
|
||||
}
|
||||
const checkAllowed = (state) => {
|
||||
handler("isAllowed", !state.map((r) => r.ingredient.isHave).includes(false))
|
||||
}
|
||||
const measureHandler = (index, value) => {
|
||||
const newState = receipt.map((r, i) => {
|
||||
if (index !== i) {
|
||||
return r
|
||||
}
|
||||
return {
|
||||
...r,
|
||||
measure: value
|
||||
}
|
||||
})
|
||||
handler("receipt", newState)
|
||||
}
|
||||
const countHandler = (index, value) => {
|
||||
const newState = receipt.map((r, i) => {
|
||||
if (index !== i) {
|
||||
return r
|
||||
}
|
||||
return {
|
||||
...r,
|
||||
count: value
|
||||
}
|
||||
})
|
||||
handler("receipt", newState)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mb={2}>
|
||||
{/*Заголовок*/}
|
||||
<Stack direction='row' justifyContent={'space-between'} sx={{mr: 1}}>
|
||||
<Typography ml={1} mt={1}>Рецепт</Typography>
|
||||
<IconButton onClick={() => addHandler()}>
|
||||
<AddIcon/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
{/*Рецепт*/}
|
||||
<Stack sx={{mr: 1}}>
|
||||
{receipt.map((r, i) => {
|
||||
return (
|
||||
<Card key={i} sx={{ml: 0, mb: 1}}>
|
||||
<Stack>
|
||||
<Stack direction='row'>
|
||||
<SelectEdit width={'calc(65% - 28px)'} array={ingredients} value={r.ingredient.name}
|
||||
handler={selectHandler} label={"Ингредиент"}
|
||||
margin={1} attributeName={i}
|
||||
/>
|
||||
<TextField sx={{width: 'calc(35% - 28px)', mt: 1}}
|
||||
label={"Кол-во"}
|
||||
variant="outlined"
|
||||
disabled
|
||||
value={r.measure}
|
||||
onChange={(e) => measureHandler(i, e.target.value)}
|
||||
/>
|
||||
<IconButton sx={{mt: 2}}
|
||||
onClick={() => removeHandler(i)}
|
||||
>
|
||||
<DeleteForeverIcon/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack direction='row' ml={1} mb={1}>
|
||||
<TextField sx={{width: 'calc(35% - 28px)', mt: 1}}
|
||||
label={"Кол-во"}
|
||||
variant="outlined"
|
||||
value={r.count}
|
||||
onChange={(e) => countHandler(i, e.target.value)}
|
||||
/>
|
||||
<SelectEdit width={'calc(65% - 28px)'} array={units} value={!r.unit ? null : r.unit.name}
|
||||
handler={unitHandler} label={"Ед."}
|
||||
margin={1} attributeName={i}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
160
front/src/components/cocktails/FilterBlock.js
Normal file
160
front/src/components/cocktails/FilterBlock.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import {Card, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import CheckMarks from "./CheckMarks";
|
||||
import Button from "@mui/material/Button";
|
||||
import * as React from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {requests} from "../../requests";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import {api} from "../../lib/clients/api";
|
||||
import {sortList} from "./sortingList";
|
||||
|
||||
const inMenuFilter = [
|
||||
{
|
||||
id: true,
|
||||
name: "Есть в меню"
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: "Нет в меню"
|
||||
}
|
||||
]
|
||||
|
||||
export function FilterBlock({filter, handleFilterChange, handleClearFilter, barmen, all}) {
|
||||
const {createError} = useAlert();
|
||||
const [glass, setGlass] = useState([]);
|
||||
const [category, setCategory] = useState([]);
|
||||
const [tags, setTags] = useState([])
|
||||
const alcohol = [
|
||||
{
|
||||
name: "Алкогольный",
|
||||
id: "alcohol1"
|
||||
},
|
||||
{
|
||||
name: "Безалкогольный",
|
||||
id: "alcohol2"
|
||||
},
|
||||
{
|
||||
name: "Опционально",
|
||||
id: "alcohol3"
|
||||
}];
|
||||
const ingredientCount = [
|
||||
{
|
||||
id: "1IngredientCount",
|
||||
name: 1
|
||||
},
|
||||
{
|
||||
id: "2IngredientCount",
|
||||
name: 2
|
||||
},
|
||||
{
|
||||
id: "3IngredientCount",
|
||||
name: 3
|
||||
},
|
||||
{
|
||||
id: "4IngredientCount",
|
||||
name: 4
|
||||
},
|
||||
{
|
||||
id: "5IngredientCount",
|
||||
name: 5
|
||||
}]
|
||||
|
||||
useEffect(() => {
|
||||
api().get(requests.bar.category)
|
||||
.then((r) => setCategory(r.data))
|
||||
.catch(() => createError("Ошибка получения категорий"))
|
||||
|
||||
api().get(requests.bar.glass)
|
||||
.then((r) => setGlass(r.data))
|
||||
.catch(() => createError("Ошибка получения посуды"))
|
||||
|
||||
api().get(requests.bar.tags)
|
||||
.then((r) => setTags(r.data))
|
||||
.catch(() => createError("Ошибка получения тегов"))
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{/*Строка поиска*/}
|
||||
<FormControl sx={{m: 1, width: '300px'}}>
|
||||
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||
<OutlinedInput
|
||||
onChange={(e) => handleFilterChange("search", e.target.value)}
|
||||
label="With normal TextField"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconButton edge="end">
|
||||
<SearchIcon/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
{/*Кнопка открытия фильтров*/}
|
||||
<Tooltip title="Filter list">
|
||||
<IconButton onClick={() => handleFilterChange("hidden", !filter.hidden)}>
|
||||
<FilterListIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/*Блок сортировки*/}
|
||||
<Box hidden={filter.hidden}>
|
||||
<Grid container>
|
||||
{/*Фильтр по алкогольности*/}
|
||||
<CheckMarks rows={sortList} name={"Сортировать по..."} handleChange={handleFilterChange}
|
||||
filterValue={filter.sorting} filterName={"sorting"} nonMulti/>
|
||||
</Grid>
|
||||
</Box>
|
||||
{/*Блок фильтров*/}
|
||||
<Box hidden={filter.hidden}>
|
||||
<Grid container>
|
||||
{/*Фильтр по меню*/}
|
||||
{(barmen && all) && (
|
||||
<CheckMarks rows={inMenuFilter} name={"Есть в меню"} filterName={"inMenu"}
|
||||
filterValue={filter.inMenu}
|
||||
handleChange={handleFilterChange}
|
||||
nonMulti nullValue
|
||||
/>
|
||||
)}
|
||||
{/*Фильтр по избранным*/}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch inputProps={{'aria-label': 'controlled'}}
|
||||
onChange={() => handleFilterChange("onlyFavourite", !filter.onlyFavourite)}
|
||||
/>
|
||||
}
|
||||
label="Только избранные"
|
||||
sx={{ml: 1}}
|
||||
/>
|
||||
{/*Фильтр по алкогольности*/}
|
||||
<CheckMarks rows={alcohol} name={"Алкогольность"} handleChange={handleFilterChange}
|
||||
filterValue={filter.alcohol} filterName={"alcohol"}/>
|
||||
{/*Фильтр по категории*/}
|
||||
{category.length > 0 && (
|
||||
<CheckMarks rows={category} name={"Категории"} filterValue={filter.category}
|
||||
filterName={"category"} handleChange={handleFilterChange}/>)}
|
||||
{/*Фильтр по посуде*/}
|
||||
{glass.length > 0 && (<CheckMarks rows={glass} name={"Подача"} handleChange={handleFilterChange}
|
||||
filterValue={filter.glass} filterName={"glass"}/>)}
|
||||
{/*Фильтр по тегам*/}
|
||||
{tags.length > 0 && (<CheckMarks rows={tags} name={"Теги"} handleChange={handleFilterChange}
|
||||
filterValue={filter.tags} filterName={"tags"}/>)}
|
||||
{/*Фильтр по нехватке ингредиентов*/}
|
||||
{/*todo: доделать эти фильтры в беке*/}
|
||||
{/*{(barmen && all) && (<CheckMarks rows={ingredientCount} name={"Не хватает ингредиентов"}*/}
|
||||
{/* handleChange={handleFilterChange}*/}
|
||||
{/* nonMulti nullValue*/}
|
||||
{/* filterValue={filter.iCount} filterName={"iCount"}/>)}*/}
|
||||
<Button onClick={() => handleClearFilter()}>Сбросить</Button>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
front/src/components/cocktails/NoResult.js
Normal file
17
front/src/components/cocktails/NoResult.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {Stack} from "@mui/material";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {CocktailItemStyled} from "./CocktailItemStyled";
|
||||
|
||||
export function NoResult({load}) {
|
||||
return (
|
||||
<Grid item lg={4} md={6} sm={12} xl={3} hidden={!load}>
|
||||
<CocktailItemStyled>
|
||||
<Stack align="center" sx={{width: "350px"}}>
|
||||
<Typography variant="h5" minHeight={'50px'} mt={2}>Нет результатов</Typography>
|
||||
<Typography>Попробуйте заглянуть позднее</Typography>
|
||||
</Stack>
|
||||
</CocktailItemStyled>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
25
front/src/components/cocktails/SelectEdit.js
Normal file
25
front/src/components/cocktails/SelectEdit.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import {FormControl, InputLabel} from "@mui/material";
|
||||
import Select from "@mui/material/Select";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import * as React from "react";
|
||||
|
||||
export function SelectEdit({label, value, array, handler, attributeName, width, margin}) {
|
||||
return (
|
||||
<FormControl sx={{width: width, m: margin}}>
|
||||
<InputLabel>{label}</InputLabel>
|
||||
<Select
|
||||
autoWidth
|
||||
label={label}
|
||||
value={!value ? "" : value}
|
||||
onChange={(e) => handler(attributeName, e.target.value)}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{array.map((c) => {
|
||||
return (<MenuItem key={c.id} value={c.name}>{c.name}</MenuItem>)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
27
front/src/components/cocktails/sortingList.js
Normal file
27
front/src/components/cocktails/sortingList.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export const sortList = [
|
||||
{
|
||||
id: "name|asc",
|
||||
name: "Название по возрастанию"
|
||||
},
|
||||
{
|
||||
id: "name|desc",
|
||||
name: "Название по убыванию"
|
||||
},
|
||||
// todo: добавить сортировки в беке
|
||||
// {
|
||||
// id: "rating.rating|desc",
|
||||
// name: "Сначала с оценкой"
|
||||
// },
|
||||
// {
|
||||
// id: "rating.rating|asc",
|
||||
// name: "Сначала без оценки"
|
||||
// },
|
||||
// {
|
||||
// id: "rating.favourite|desc",
|
||||
// name: "Сначала избранные"
|
||||
// },
|
||||
// {
|
||||
// id: "rating.favourite|asc",
|
||||
// name: "Сначала не избранные"
|
||||
// }
|
||||
]
|
||||
12
front/src/components/core/Loading.js
Normal file
12
front/src/components/core/Loading.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {Backdrop, CircularProgress} from "@mui/material";
|
||||
|
||||
export function Loading({loading}) {
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1}}
|
||||
open={loading}
|
||||
>
|
||||
<CircularProgress color="inherit"/>
|
||||
</Backdrop>
|
||||
);
|
||||
}
|
||||
10
front/src/components/core/LocalizationProvider.js
Normal file
10
front/src/components/core/LocalizationProvider.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import {LocalizationProvider as Provider} from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import * as React from 'react';
|
||||
import 'dayjs/locale/ru'
|
||||
|
||||
export function LocalizationProvider({children}) {
|
||||
return (
|
||||
<Provider dateAdapter={AdapterDayjs} adapterLocale="ru-Ru">{children}</Provider>
|
||||
);
|
||||
}
|
||||
38
front/src/components/core/Logo.js
Normal file
38
front/src/components/core/Logo.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useColorScheme } from '@mui/material/styles';
|
||||
import {NoSsr} from "./NoSsr";
|
||||
|
||||
const HEIGHT = 60;
|
||||
const WIDTH = 60;
|
||||
|
||||
export function Logo({ color = 'dark', emblem, height = HEIGHT, width = WIDTH }) {
|
||||
let url;
|
||||
|
||||
if (emblem) {
|
||||
url = color === 'light' ? '/assets/logo-emblem.svg' : '/assets/logo-emblem--dark.svg';
|
||||
} else {
|
||||
url = color === 'light' ? '/assets/logo.svg' : '/assets/logo--dark.svg';
|
||||
}
|
||||
|
||||
return <Box alt="logo" component="img" height={height} src={url} width={width} />;
|
||||
}
|
||||
|
||||
export function DynamicLogo({
|
||||
colorDark = 'light',
|
||||
colorLight = 'dark',
|
||||
height = HEIGHT,
|
||||
width = WIDTH,
|
||||
...props
|
||||
}) {
|
||||
const { colorScheme } = useColorScheme();
|
||||
const color = colorScheme === 'dark' ? colorDark : colorLight;
|
||||
|
||||
return (
|
||||
<NoSsr fallback={<Box sx={{ height: `${height}px`, width: `${width}px` }} />}>
|
||||
<Logo color={color} height={height} width={width} {...props} />
|
||||
</NoSsr>
|
||||
);
|
||||
}
|
||||
9
front/src/components/core/ModalDialogStyled.js
Normal file
9
front/src/components/core/ModalDialogStyled.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import {styled} from "@mui/material/styles";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
|
||||
export const ModalDialogStyled = styled(Dialog)(({theme}) => ({
|
||||
backdrop: {
|
||||
margin: '4px',
|
||||
border: 'solid',
|
||||
},
|
||||
}));
|
||||
25
front/src/components/core/NoSsr.js
Normal file
25
front/src/components/core/NoSsr.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
|
||||
|
||||
export function NoSsr(props) {
|
||||
const {children, defer = false, fallback = null} = props;
|
||||
const [mountedState, setMountedState] = React.useState(false);
|
||||
|
||||
useEnhancedEffect(() => {
|
||||
if (!defer) {
|
||||
setMountedState(true);
|
||||
}
|
||||
}, [defer]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (defer) {
|
||||
setMountedState(true);
|
||||
}
|
||||
}, [defer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mountedState ? children : fallback}
|
||||
</>
|
||||
)
|
||||
}
|
||||
24
front/src/components/core/TabPanel.js
Normal file
24
front/src/components/core/TabPanel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import PropTypes from "prop-types";
|
||||
import * as React from "react";
|
||||
|
||||
export function CustomTabPanel(props) {
|
||||
const {children, value, index, ...other} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CustomTabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
index: PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
};
|
||||
81
front/src/components/core/ThemeSwitch.js
Normal file
81
front/src/components/core/ThemeSwitch.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import {styled, useColorScheme} from "@mui/material/styles";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export function ThemeSwitch() {
|
||||
const {mode, setMode} = useColorScheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
// backgroundColor: 'var(--mui-palette-neutral-950)',
|
||||
border: '1px solid var(--mui-palette-neutral-700)',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
p: '4px 12px',
|
||||
}}
|
||||
>
|
||||
<StyledSwitch
|
||||
checked={mode === 'dark'}
|
||||
onChange={(e) => setMode(e.target.checked ? 'dark' : 'light')}
|
||||
inputProps={{'aria-label': 'controlled'}}
|
||||
/>
|
||||
<Box sx={{flex: '1 1 auto'}}>
|
||||
<Typography color="inherit" variant="subtitle1">
|
||||
{(mode === 'dark' ? "Темная " : "Светлая ") + "тема"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledSwitch = styled(Switch)(({theme}) => ({
|
||||
width: 62,
|
||||
height: 34,
|
||||
padding: 7,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
margin: 1,
|
||||
padding: 0,
|
||||
transform: 'translateX(6px)',
|
||||
'&.Mui-checked': {
|
||||
color: '#fff',
|
||||
transform: 'translateX(22px)',
|
||||
'& .MuiSwitch-thumb:before': {
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||
'#fff',
|
||||
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
|
||||
},
|
||||
'& + .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
},
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
|
||||
width: 32,
|
||||
height: 32,
|
||||
'&::before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||
'#fff',
|
||||
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
borderRadius: 20 / 2,
|
||||
},
|
||||
}));
|
||||
92
front/src/components/core/UserPopover.js
Normal file
92
front/src/components/core/UserPopover.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {SignOut as SignOutIcon} from '@phosphor-icons/react/dist/ssr/SignOut';
|
||||
import {logger} from "../../lib/DefaultLogger";
|
||||
import {useAuth} from "../../hooks/useAuth";
|
||||
import {authClient} from "../../lib/clients/AuthClient";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
|
||||
export function UserPopover({anchorEl, onClose, open}) {
|
||||
const {checkSession} = useAuth();
|
||||
const {user, session} = useUser();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const handleSignOut = React.useCallback(async () => {
|
||||
try {
|
||||
const {error} = await authClient.signOut();
|
||||
|
||||
if (error) {
|
||||
logger.error('Sign out error', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh the auth state
|
||||
await checkSession?.();
|
||||
|
||||
// UserProvider, for this case, will not refresh the router and we need to do it manually
|
||||
window.location.reload();
|
||||
// After refresh, AuthGuard will handle the redirect
|
||||
} catch (err) {
|
||||
logger.error('Sign out error', err);
|
||||
}
|
||||
}, [checkSession, location]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{horizontal: 'left', vertical: 'bottom'}}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
slotProps={{paper: {sx: {width: '240px'}}}}
|
||||
>
|
||||
<Box sx={{p: '16px 20px '}}>
|
||||
{userDescriptor(user, session)}
|
||||
</Box>
|
||||
<Divider/>
|
||||
<MenuList disablePadding sx={{p: '8px', '& .MuiMenuItem-root': {borderRadius: 1}}}>
|
||||
{/*<MenuItem component={'a'} href={paths.dashboard.settings} onClick={onClose}>*/}
|
||||
{/* <ListItemIcon>*/}
|
||||
{/* <GearSixIcon fontSize="var(--icon-fontSize-md)"/>*/}
|
||||
{/* </ListItemIcon>*/}
|
||||
{/* Настройки*/}
|
||||
{/*</MenuItem>*/}
|
||||
{/*<MenuItem component={'a'} href={paths.dashboard.account} onClick={onClose}>*/}
|
||||
{/* <ListItemIcon>*/}
|
||||
{/* <UserIcon fontSize="var(--icon-fontSize-md)"/>*/}
|
||||
{/* </ListItemIcon>*/}
|
||||
{/* Профиль*/}
|
||||
{/*</MenuItem>*/}
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<ListItemIcon>
|
||||
<SignOutIcon fontSize="var(--icon-fontSize-md)"/>
|
||||
</ListItemIcon>
|
||||
Выход
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function userDescriptor(user, session) {
|
||||
if (!user) {
|
||||
return (<Typography variant="subtitle1">Ошибка загрузки данных</Typography>);
|
||||
}
|
||||
|
||||
const open = (session.isActive && user.invited) ? "открыт" : "закрыт";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle1">{user.name + " " + user.lastName}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">{user.id}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">{`Бар ${open}`}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
29
front/src/components/core/descendingComparator.js
Normal file
29
front/src/components/core/descendingComparator.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export function descendingComparator(a, b, orderBy, lastOrder) {
|
||||
if (getValue(b, orderBy) < getValue(a, orderBy)) {
|
||||
return -1;
|
||||
}
|
||||
if (getValue(b, orderBy) > getValue(a, orderBy)) {
|
||||
return 1;
|
||||
}
|
||||
if (lastOrder && orderBy !== lastOrder) {
|
||||
if (getValue(b, lastOrder) < getValue(a, lastOrder)) {
|
||||
return 1;
|
||||
}
|
||||
if (getValue(b, lastOrder) > getValue(a, lastOrder)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getValue(obj, orderBy) {
|
||||
if (!orderBy) {
|
||||
return obj;
|
||||
}
|
||||
const split = orderBy.split(".")
|
||||
let res = obj[split[0]];
|
||||
for (let i = 1; i < split.length; i++) {
|
||||
res = res[split[i]];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
10
front/src/components/core/getComparator.js
Normal file
10
front/src/components/core/getComparator.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {descendingComparator} from "./descendingComparator";
|
||||
|
||||
export function getComparator(order, orderBy, lastOrder) {
|
||||
if(!order) {
|
||||
order = "asc"
|
||||
}
|
||||
return order === 'desc'
|
||||
? (a, b) => descendingComparator(a, b, orderBy, lastOrder)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy, lastOrder);
|
||||
}
|
||||
14
front/src/components/core/groupByForLoop.js
Normal file
14
front/src/components/core/groupByForLoop.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export const groupByForLoop = (arr, prop) => {
|
||||
const grouped = new Map();
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let key = arr[i][prop];
|
||||
if (!key) {
|
||||
key = "Без категории"
|
||||
}
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, []);
|
||||
}
|
||||
grouped.get(key).push(arr[i]);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
37
front/src/components/core/navIcons.js
Normal file
37
front/src/components/core/navIcons.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import {ChartPie as ChartPieIcon} from '@phosphor-icons/react/dist/ssr/ChartPie';
|
||||
import {GearSix as GearSixIcon} from '@phosphor-icons/react/dist/ssr/GearSix';
|
||||
import {PlugsConnected as PlugsConnectedIcon} from '@phosphor-icons/react/dist/ssr/PlugsConnected';
|
||||
import {User as UserIcon} from '@phosphor-icons/react/dist/ssr/User';
|
||||
import {Users as UsersIcon} from '@phosphor-icons/react/dist/ssr/Users';
|
||||
import {XSquare} from '@phosphor-icons/react/dist/ssr/XSquare';
|
||||
import {
|
||||
Basket,
|
||||
BookOpen,
|
||||
Books,
|
||||
Cheers,
|
||||
CoffeeBean,
|
||||
Coins,
|
||||
Martini,
|
||||
Storefront,
|
||||
Users,
|
||||
Wallet
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export const navIcons = {
|
||||
'menu': BookOpen,
|
||||
'list': Books,
|
||||
'storefront': Storefront,
|
||||
'wallet': Wallet,
|
||||
'cocktail': Martini,
|
||||
'visitors': Users,
|
||||
'orders': Cheers,
|
||||
'basket': Basket,
|
||||
'coins': Coins,
|
||||
'ingredients': CoffeeBean,
|
||||
'chart-pie': ChartPieIcon,
|
||||
'gear-six': GearSixIcon,
|
||||
'plugs-connected': PlugsConnectedIcon,
|
||||
'x-square': XSquare,
|
||||
user: UserIcon,
|
||||
users: UsersIcon,
|
||||
}
|
||||
6
front/src/components/core/tabProps.js
Normal file
6
front/src/components/core/tabProps.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export function a11yProps(index) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
65
front/src/components/navigation/MainNav.js
Normal file
65
front/src/components/navigation/MainNav.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import {List as ListIcon} from '@phosphor-icons/react/dist/ssr/List';
|
||||
// import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||
|
||||
import {usePopover} from "../../hooks/usePopover";
|
||||
import {MobileNav} from "./MobileNav";
|
||||
import {UserPopover} from "../core/UserPopover";
|
||||
// import Tooltip from "@mui/material/Tooltip";
|
||||
// import {Badge} from "@mui/material";
|
||||
// import {useAlert} from "../../hooks/useAlert";
|
||||
|
||||
export function MainNav() {
|
||||
const [openNav, setOpenNav] = React.useState(false);
|
||||
// const {notImplement} = useAlert();
|
||||
|
||||
const userPopover = usePopover();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
component="header"
|
||||
sx={{
|
||||
borderBottom: '1px solid var(--mui-palette-divider)',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 'var(--mui-zIndex-appBar)',
|
||||
height: '64px'
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={3}
|
||||
sx={{alignItems: 'center', justifyContent: 'space-between', minHeight: '64px', px: 2}}>
|
||||
<Stack sx={{alignItems: 'center'}} direction="row" spacing={3}>
|
||||
<IconButton onClick={() => setOpenNav(true)} sx={{display: {xl: 'none'}}}>
|
||||
<ListIcon/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack sx={{alignItems: 'center'}} direction="row" spacing={2}>
|
||||
{/*<Tooltip title="Уведомления" onClick={() => notImplement()}>*/}
|
||||
{/* <Badge badgeContent={10} color="success" variant="standart">*/}
|
||||
{/* <IconButton>*/}
|
||||
{/* <NotificationsIcon/>*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </Badge>*/}
|
||||
{/*</Tooltip>*/}
|
||||
<Avatar onClick={userPopover.handleOpen} ref={userPopover.anchorRef} src="/assets/avatar.png"
|
||||
sx={{cursor: 'pointer'}}/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
<UserPopover anchorEl={userPopover.anchorRef.current} onClose={userPopover.handleClose}
|
||||
open={userPopover.open}/>
|
||||
<MobileNav
|
||||
onClose={() => {
|
||||
setOpenNav(false);
|
||||
}}
|
||||
open={openNav}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
front/src/components/navigation/MobileNav.js
Normal file
38
front/src/components/navigation/MobileNav.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import {NavigationMenu} from "./NavigationMenu";
|
||||
|
||||
//Боковое меню
|
||||
export function MobileNav({open, onClose}) {
|
||||
return (
|
||||
<Drawer
|
||||
PaperProps={{
|
||||
sx: {
|
||||
'--MobileNav-background': 'var(--mui-palette-neutral-950)',
|
||||
'--MobileNav-color': 'var(--mui-palette-common-white)',
|
||||
'--NavItem-color': 'var(--mui-palette-neutral-300)',
|
||||
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
|
||||
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
|
||||
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
|
||||
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
|
||||
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
|
||||
bgcolor: 'var(--MobileNav-background)',
|
||||
color: 'var(--MobileNav-color)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '100%',
|
||||
scrollbarWidth: 'none',
|
||||
width: 'var(--MobileNav-width)',
|
||||
zIndex: 'var(--MobileNav-zIndex)',
|
||||
'&::-webkit-scrollbar': {display: 'none'},
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
>
|
||||
<NavigationMenu/>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
74
front/src/components/navigation/NavItem.js
Normal file
74
front/src/components/navigation/NavItem.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {isNavItemActive} from "../../lib/isNavItemActive";
|
||||
import {navIcons} from "../core/navIcons";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
export function renderNavItems({items = [], pathname}) {
|
||||
const children = items.reduce((acc, curr) => {
|
||||
const {key, ...item} = curr;
|
||||
acc.push(<NavItem key={key} pathname={pathname} {...item} />);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack key={"stack-NavItem-key"} component="ul" spacing={1} sx={{listStyle: 'none', m: 0, p: 0}}>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({disabled, external, href, icon, matcher, pathname, title}) {
|
||||
const active = isNavItemActive({disabled, external, href, matcher, pathname});
|
||||
const Icon = icon ? navIcons[icon] : null;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link to={href} style={{ textDecoration: 'none' }}>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
borderRadius: 1,
|
||||
color: 'var(--NavItem-color)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flex: '0 0 auto',
|
||||
gap: 1,
|
||||
p: '6px 16px',
|
||||
position: 'relative',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
...(disabled && {
|
||||
bgcolor: 'var(--NavItem-disabled-background)',
|
||||
color: 'var(--NavItem-disabled-color)',
|
||||
cursor: 'not-allowed',
|
||||
}),
|
||||
...(active && {
|
||||
bgcolor: 'var(--NavItem-active-background)',
|
||||
color: 'var(--NavItem-active-color)'
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box sx={{alignItems: 'center', display: 'flex', justifyContent: 'center', flex: '0 0 auto'}}>
|
||||
{Icon ? (
|
||||
<Icon
|
||||
fill={active ? 'var(--NavItem-icon-active-color)' : 'var(--NavItem-icon-color)'}
|
||||
fontSize="var(--icon-fontSize-md)"
|
||||
weight={active ? 'fill' : undefined}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
<Box sx={{flex: '1 1 auto'}}>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{color: 'inherit', fontSize: '0.875rem', fontWeight: 500, lineHeight: '28px'}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
56
front/src/components/navigation/NavigationMenu.js
Normal file
56
front/src/components/navigation/NavigationMenu.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import {ThemeSwitch} from "../core/ThemeSwitch";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import {renderNavItems} from "./NavItem";
|
||||
import {navItems} from "../../navItems";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
function renderSpecialItems(items, label, pathname) {
|
||||
return (
|
||||
<Box>
|
||||
<hr/>
|
||||
<Typography pl={"20px"} pb={1} variant="subtitle2" color="textSecondary">{label}</Typography>
|
||||
{renderNavItems({items: items, pathname: pathname})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavigationMenu() {
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const {user} = useUser();
|
||||
const [items, setItems] = useState(null)
|
||||
|
||||
const userChild = navItems.filter((item) => !item.forBarmen && !item.forAdmin)
|
||||
const barmenChild = navItems.filter((item) => item.forBarmen)
|
||||
const adminChild = navItems.filter((item) => item.forAdmin)
|
||||
|
||||
useEffect(() => {
|
||||
const role = !user ? "USER" : Object.keys(user).length === 0 ? "USER" : user.role
|
||||
const newState = (
|
||||
<Box component="nav" sx={{flex: '1 1 auto', p: '12px'}}>
|
||||
{renderNavItems({items: userChild, pathname: pathname})}
|
||||
{role !== "USER" && renderSpecialItems(barmenChild, "Для бармена:", pathname)}
|
||||
{role === "ADMIN" && renderSpecialItems(adminChild, "Для админа", pathname)}
|
||||
</Box>
|
||||
)
|
||||
setItems(newState)
|
||||
// eslint-disable-next-line
|
||||
}, [user, pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*верхняя стопка*/}
|
||||
<Stack spacing={2} sx={{p: 2, height: '63px'}}>
|
||||
<ThemeSwitch/>
|
||||
</Stack>
|
||||
<Divider sx={{borderColor: 'var(--mui-palette-neutral-700)'}}/>
|
||||
{/*меню навигации*/}
|
||||
{items}
|
||||
</>
|
||||
)
|
||||
}
|
||||
37
front/src/components/navigation/SideNav.js
Normal file
37
front/src/components/navigation/SideNav.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import {NavigationMenu} from "./NavigationMenu";
|
||||
|
||||
export function SideNav() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
'--SideNav-background': 'var(--mui-palette-neutral-950)',
|
||||
'--SideNav-color': 'var(--mui-palette-common-white)',
|
||||
'--NavItem-color': 'var(--mui-palette-neutral-300)',
|
||||
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
|
||||
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
|
||||
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
|
||||
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
|
||||
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
|
||||
bgcolor: 'var(--SideNav-background)',
|
||||
color: 'var(--SideNav-color)',
|
||||
display: {xs: 'none', xl: 'flex'},
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
maxWidth: '100%',
|
||||
position: 'fixed',
|
||||
scrollbarWidth: 'none',
|
||||
top: 0,
|
||||
width: 'var(--SideNav-width)',
|
||||
zIndex: 'var(--SideNav-zIndex)',
|
||||
'&::-webkit-scrollbar': {display: 'none'},
|
||||
}}
|
||||
>
|
||||
<NavigationMenu/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
113
front/src/components/orders/EnhancedTable.js
Normal file
113
front/src/components/orders/EnhancedTable.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TablePagination from "@mui/material/TablePagination";
|
||||
import {getComparator} from "../core/getComparator";
|
||||
import {EnhancedTableToolbar} from "./EnhancedTableToolbar";
|
||||
import {EnhancedTableHead} from "./EnhancedTableHead";
|
||||
|
||||
export default function EnhancedTable({name, rows, cells, handleSelect, filterField, filterEqual, filterValue}) {
|
||||
//сортировка убывание/возрастание
|
||||
const [order, setOrder] = React.useState('desc');
|
||||
//По какому полю сортируем
|
||||
const [orderBy, setOrderBy] = React.useState('id');
|
||||
//выбранная страница
|
||||
const [page, setPage] = React.useState(0);
|
||||
//количество элементов на странице
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
|
||||
const handleRequestSort = (event, property) => {
|
||||
const isAsc = orderBy === property && order === 'asc';
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(property);
|
||||
};
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
const getTableValue = (obj, index) => {
|
||||
let indexArr = index.split(".");
|
||||
let object = obj;
|
||||
for (let i of indexArr) {
|
||||
object = object[i];
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
const visibleRows = React.useMemo(() =>
|
||||
[...rows]
|
||||
.filter((row) => {
|
||||
if (!filterField) {
|
||||
return true;
|
||||
}
|
||||
for (let field of filterField) {
|
||||
for (let value of filterValue) {
|
||||
let eq = (row[field] === value) === filterEqual;
|
||||
if (!eq) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort(getComparator(order, orderBy))
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage),
|
||||
[order, orderBy, page, rowsPerPage, rows, filterEqual, filterField, filterValue],
|
||||
);
|
||||
|
||||
const renderTable = (row) => {
|
||||
// const isItemSelected = selected.includes(row.id);
|
||||
const isItemSelected = false;
|
||||
return (
|
||||
<TableRow hover onClick={() => handleSelect(row)} role="checkbox"
|
||||
aria-checked={isItemSelected} tabIndex={-1} key={row.id} selected={isItemSelected}
|
||||
sx={{cursor: 'pointer'}}>
|
||||
{cells.map((cell) => {
|
||||
return (
|
||||
<TableCell key={cell.id} sx={{maxWidth: cell.width}}>{getTableValue(row, cell.id)}</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
const emptyRow = () => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={cells.length}>
|
||||
Нет заказов
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{width: '100%'}}>
|
||||
<Paper sx={{width: '100%', mb: 2}}>
|
||||
<EnhancedTableToolbar numSelected={0} name={name}/>
|
||||
<TableContainer>
|
||||
<Table sx={{width: 'calc(100% - 30px)'}} aria-labelledby="tableTitle" size="medium">
|
||||
<EnhancedTableHead numSelected={0} order={order} orderBy={orderBy}
|
||||
onRequestSort={handleRequestSort}
|
||||
rowCount={rows.length} cells={cells}/>
|
||||
<TableBody>
|
||||
{visibleRows.map((row) => renderTable(row))}
|
||||
{visibleRows.length === 0 && emptyRow()}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination rowsPerPageOptions={[5, 10, 25]} component="div" count={visibleRows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
32
front/src/components/orders/EnhancedTableHead.js
Normal file
32
front/src/components/orders/EnhancedTableHead.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||
import Box from "@mui/material/Box";
|
||||
import {visuallyHidden} from "@mui/utils";
|
||||
import * as React from "react";
|
||||
|
||||
export function EnhancedTableHead(props) {
|
||||
const {order, orderBy, onRequestSort, cells} = props;
|
||||
const createSortHandler = (property) => (event) => {onRequestSort(event, property);};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{cells.map((headCell) => (
|
||||
<TableCell key={headCell.id} align={"left"} padding={headCell.disablePadding ? 'none' : 'normal'}
|
||||
sortDirection={orderBy === headCell.id ? order : false} sx={{pl: 1, maxWidth: headCell.width}}>
|
||||
<TableSortLabel active={orderBy === headCell.id} direction={orderBy === headCell.id ? order : 'asc'} onClick={createSortHandler(headCell.id)}>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<Box component="span" sx={visuallyHidden}>
|
||||
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
||||
</Box>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
64
front/src/components/orders/EnhancedTableToolbar.js
Normal file
64
front/src/components/orders/EnhancedTableToolbar.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import {alpha} from "@mui/material/styles";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
import PropTypes from "prop-types";
|
||||
import * as React from "react";
|
||||
|
||||
export function EnhancedTableToolbar(props) {
|
||||
const { numSelected, name } = props;
|
||||
return (
|
||||
<Toolbar
|
||||
sx={[
|
||||
{
|
||||
pl: { sm: 2 },
|
||||
pr: { xs: 1, sm: 1 },
|
||||
},
|
||||
numSelected > 0 && {
|
||||
bgcolor: (theme) =>
|
||||
alpha(theme.palette.primary.main, theme.palette.action.activatedOpacity),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{numSelected > 0 ? (
|
||||
<Typography
|
||||
sx={{ flex: '1 1 100%' }}
|
||||
color="inherit"
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
>
|
||||
{numSelected} selected
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
sx={{ flex: '1 1 100%' }}
|
||||
variant="h6"
|
||||
id="tableTitle"
|
||||
component="div"
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
)}
|
||||
{numSelected > 0 ? (
|
||||
<Tooltip title="Delete">
|
||||
<IconButton>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Filter list">
|
||||
<IconButton>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
||||
EnhancedTableToolbar.propTypes = {
|
||||
numSelected: PropTypes.number.isRequired,
|
||||
};
|
||||
95
front/src/components/orders/OrderModal.js
Normal file
95
front/src/components/orders/OrderModal.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as React from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {ButtonGroup} from "@mui/material";
|
||||
import {requests} from "../../requests";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import {api} from "../../lib/clients/api";
|
||||
|
||||
function renderButtons(row, my, handleChange) {
|
||||
if (my) {
|
||||
if (row.status === "NEW") {
|
||||
return (
|
||||
<ButtonGroup variant="contained">
|
||||
<Button color="error" onClick={() => handleChange(row, "CANCEL")}>Отмена</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<ButtonGroup variant="contained">
|
||||
<Button color="success" onClick={() => handleChange(row, "DONE")}>Выполнен</Button>
|
||||
<Button color="error" onClick={() => handleChange(row, "CANCEL")}>Отмена</Button>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default function OrderModal({row, handleClose, open, handleChange, my}) {
|
||||
const [receipt, setReceipt] = useState([]);
|
||||
const {createError} = useAlert();
|
||||
|
||||
useEffect(() => {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
api().get(requests.bar.receipts + row.cocktail.id)
|
||||
.then((r) => setReceipt(r.data))
|
||||
.catch(() => createError("Ошибка получения рецепта"))
|
||||
// eslint-disable-next-line
|
||||
}, [row]);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
fullWidth={true}
|
||||
maxWidth="350px"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<DialogTitle>{"Заказ №" + row.id}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{row.cocktail.name}</DialogContentText>
|
||||
<DialogContentText>{row.cocktail.alcoholic + " " + row.cocktail.category}</DialogContentText>
|
||||
<DialogContentText>{"для: " + row.visitor.name + " " + row.visitor.lastName}</DialogContentText>
|
||||
<Box noValidate component="form"
|
||||
sx={{display: 'flex', flexDirection: 'column', m: 'auto', width: 'fit-content',}}>
|
||||
<Stack>
|
||||
<img src={row.cocktail.image} alt={row.cocktail.name} loading={"eager"} width={"300"}/>
|
||||
<Typography>Ингредиенты:</Typography>
|
||||
<Stack pl={1}>
|
||||
{receipt.map((r) => {
|
||||
return (<Typography key={r.id}>{`${r.ingredient.name} - ${r.measure}`}</Typography>)
|
||||
})}
|
||||
</Stack>
|
||||
<Typography>Инструкция:</Typography>
|
||||
<Typography pl={1}>{row.cocktail.instructions}</Typography>
|
||||
|
||||
{row.cocktail.video && (<iframe width="350" /*height="315"*/
|
||||
src={row.cocktail.video}
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen></iframe>)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{justifyContent: "space-between"}}>
|
||||
{renderButtons(row, my, handleChange)}
|
||||
<Button onClick={handleClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
9
front/src/components/orders/createHeadCelll.js
Normal file
9
front/src/components/orders/createHeadCelll.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function createHeadCell(id, numeric, padding, label, width) {
|
||||
return {
|
||||
id: id,
|
||||
numeric: numeric,
|
||||
disablePadding: padding,
|
||||
label: label,
|
||||
width: width
|
||||
}
|
||||
}
|
||||
49
front/src/components/visitor/VisitorItem.js
Normal file
49
front/src/components/visitor/VisitorItem.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {Card, FormControlLabel} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import * as React from "react";
|
||||
|
||||
export function VisitorItem({visitor, changeHandler, open}) {
|
||||
|
||||
const getRole = (role) => {
|
||||
switch (role) {
|
||||
case "USER":
|
||||
return 'Посетитель';
|
||||
case "BARMEN":
|
||||
return 'Бармен';
|
||||
case "ADMIN":
|
||||
return 'Админ';
|
||||
default:
|
||||
return "Посетитель";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{mb: 1, p: 1, borderRadius: '10px', maxWidth: '600px'}}>
|
||||
<Stack>
|
||||
<Typography variant='h6'>{`${visitor.name} ${!visitor.lastName ? "" : visitor.lastName}`}</Typography>
|
||||
<Box display='flex' justifyContent='flex-end'>
|
||||
<Typography>{getRole(visitor.role)}</Typography>
|
||||
</Box>
|
||||
<Box display='flex' justifyContent='flex-start'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={visitor.invited}
|
||||
disabled={open}
|
||||
onChange={() => changeHandler(visitor)}
|
||||
/>}
|
||||
label="Приглашен" labelPlacement='start'/>
|
||||
</Box>
|
||||
<Box display='flex' justifyContent='flex-end'>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color={visitor.isActive ? 'green' : 'red'}
|
||||
>{visitor.isActive ? "В баре" : "Не вошел в бар"}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user