Merge branch 'dev' into 'main'

Добавил jest для тестирования, использовал

See merge request frontend/admin!5
This commit is contained in:
Mikhail 2023-03-15 10:22:50 +00:00
commit e902e4b8fd
24 changed files with 47954 additions and 9287 deletions

38015
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -22,10 +22,13 @@
"@types/react": "^18.0.18", "@types/react": "^18.0.18",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"axios": "^1.3.4",
"craco": "^0.0.3", "craco": "^0.0.3",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"formik": "^2.2.9",
"moment": "^2.29.4", "moment": "^2.29.4",
"nanoid": "^4.0.1", "nanoid": "^4.0.1",
"notistack": "^3.0.1",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

@ -0,0 +1,44 @@
const puppeteer = require('puppeteer');
const url = "http://localhost:3000/users";
const urlMass = ['/users','/tariffs','/discounts','/promocode','/support', '/entities'];
jest.setTimeout(1000 * 60 * 5);
let browser;
let page;
describe('Тест', (() => {
beforeAll(async()=>{
browser = puppeteer.launch({headless:true});
page = browser.newPage();
page.goto(url);
// Set screen size
page.setViewport({width: 1080, height: 1024});
})
afterAll(() => browser.quit());
test('Тест меню',async () => {
// Ждем загрузки менюшек
page.waitForSelector('.menu')
// Берем все ссылки с кнопок, у которых есть класс menu и вставляем в массив
let menuLink = page.evaluate(()=>{
let menuArray = document.querySelectorAll('.menu')
let Urls = Object.values(menuArray).map(
menuItem => (
menuItem.href.slice(menuItem.href.lastIndexOf('/'))
)
)
return Urls
})
// Проверяем, какие ссылки есть в нашем массиве, а каких нет
for (let i = 0; i < menuLink.length; i++) {
expect(urlMass.find((elem)=>elem===menuLink[i])).toBe(true)
}
})
}))

@ -1,38 +1,61 @@
import * as React from "react"; import * as React from "react";
import CssBaseline from "@mui/material/CssBaseline";
import { SnackbarProvider } from 'notistack';
import { ThemeProvider } from '@mui/material/styles';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route, Outlet, Navigate } from "react-router-dom";
import Authorization from "./pages/Authorization"; import theme from "./theme";
import Sections from "./pages/Sections"; import PublicRoute from "@kitUI/publicRoute";
import LoggedIn from "./pages/dashboard"; import PrivateRoute from "@kitUI/privateRoute";
import Error404 from "./pages/Error404"; import Signin from "@pages/Authorization/signin";
import Signup from "@pages/Authorization/signup";
import Restore from "@pages/Authorization/restore";
import Sections from "@pages/Sections";
import Dashboard from "@pages/dashboard";
import Error404 from "@pages/Error404";
import Users from "@pages/dashboard/Content/Users";
import Entities from "@pages/dashboard/Content/Entities";
import Tariffs from "@pages/dashboard/Content/Tariffs";
import DiscountManagement from "@pages/dashboard/Content/DiscountManagement";
import PromocodeManagement from "@pages/dashboard/Content/PromocodeManagement";
import Support from "@pages/dashboard/Content/Support";
const componentsArray = [
["/users", <Users />],
["/entities",<Entities />],
["/tariffs", <Tariffs />],
["/discounts", <DiscountManagement />],
["/promocode", <PromocodeManagement />],
["/support", <Support />]
]
const container = document.getElementById('root'); const container = document.getElementById('root');
const root = createRoot(container!); const root = createRoot(container!);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<CssBaseline />
<ThemeProvider theme={theme}>
<SnackbarProvider>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={ <Authorization /> } /> <Route path="/" element={<PublicRoute><Signin /></PublicRoute> } />
<Route path="/dispatch" element={ <Sections /> } /> <Route path="/signin" element={ <PublicRoute><Signin /></PublicRoute> } />
<Route path="/signup" element={ <PublicRoute><Signup /></PublicRoute> } />
<Route path="/restore" element={ <PublicRoute><Restore /></PublicRoute> } />
<Route path="/dispatch" element={ <PublicRoute><Sections /></PublicRoute> } />
<Route element={<PrivateRoute><Dashboard/></PrivateRoute>}>
{componentsArray.map((e:any, i) => (
<Route key={i} path={e[0]} element={e[1]} />
))}
</Route>
<Route path="/users" element={ <LoggedIn section={1} /> } />
<Route path="/entities" element={ <LoggedIn section={2} /> } />
<Route path="/tariffs" element={ <LoggedIn section={3} /> } />
<Route path="/discounts" element={ <LoggedIn section={4} /> } />
<Route path="/promocode" element={ <LoggedIn section={5} /> } />
<Route path="/support" element={ <LoggedIn section={8} /> } />
<Route path="/modalAdmin" element={ <LoggedIn section={-1} /> } />
<Route path="/modalUser" element={ <LoggedIn section={-1} /> } />
<Route path="/modalEntities" element={ <LoggedIn section={-1} /> } />
<Route <Route
path="*" path="*"
element={ <Error404 /> } element={ <Error404 /> }
/> />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</SnackbarProvider>
</ThemeProvider>
</React.StrictMode> </React.StrictMode>
); );

@ -1,5 +1,5 @@
import theme from "@theme"; import theme from "@theme";
import { Button, Paper, Box, Typography, TableHead, TableRow, TableCell, TableBody, Table, Tooltip, Alert } from "@mui/material"; import { Button, Paper, Box, Typography, TableHead, TableRow, TableCell, TableBody, Table, Tooltip, Alert, Checkbox, FormControlLabel } from "@mui/material";
import Input from "@kitUI/input"; import Input from "@kitUI/input";
import { useCartStore } from "@root/stores/cart"; import { useCartStore } from "@root/stores/cart";
import { useState } from "react"; import { useState } from "react";
@ -22,7 +22,7 @@ export default function Cart({ selectedTariffs }: Props) {
const setCartTotal = useCartStore(store => store.setCartTotal); const setCartTotal = useCartStore(store => store.setCartTotal);
const [couponField, setCouponField] = useState<string>(""); const [couponField, setCouponField] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
// const [coupon, setCoupon] = useState<string | undefined>(); const [isNonCommercial, setIsNonCommercial] = useState<boolean>(false);
const cartRows = cartTotal?.items.map(cartItemTotal => { const cartRows = cartTotal?.items.map(cartItemTotal => {
const service = cartItemTotal.tariff.privilege.serviceKey; const service = cartItemTotal.tariff.privilege.serviceKey;
@ -63,8 +63,8 @@ export default function Cart({ selectedTariffs }: Props) {
}; };
}); });
const cartDiscounts = cartTotal?.envolvedCartDiscounts const cartDiscounts = cartTotal?.envolvedCartDiscounts;
const cartDiscountsResultFactor = cartDiscounts && cartDiscounts.reduce((acc, discount) => acc * findDiscountFactor(discount), 1); const cartDiscountsResultFactor = cartDiscounts && cartDiscounts?.length > 1 && cartDiscounts.reduce((acc, discount) => acc * findDiscountFactor(discount), 1);
const envolvedCartDiscountsElement = cartDiscounts && ( const envolvedCartDiscountsElement = cartDiscounts && (
<Box sx={{ <Box sx={{
@ -89,7 +89,7 @@ export default function Cart({ selectedTariffs }: Props) {
function handleCalcCartClick() { function handleCalcCartClick() {
const cartTariffs = tariffs.filter(tariff => selectedTariffs.includes(tariff.id)); const cartTariffs = tariffs.filter(tariff => selectedTariffs.includes(tariff.id));
const cartItems = cartTariffs.map(tariff => createCartItem(tariff)); const cartItems = cartTariffs.map(tariff => createCartItem(tariff));
const cartData = calcCartData(testUser, cartItems, discounts, couponField); const cartData = calcCartData(testUser, cartItems, discounts, isNonCommercial, couponField);
if (cartData instanceof Error) { if (cartData instanceof Error) {
setErrorMessage(cartData.message); setErrorMessage(cartData.message);
@ -118,8 +118,29 @@ export default function Cart({ selectedTariffs }: Props) {
</Typography> </Typography>
<Paper <Paper
variant="bar" variant="bar"
sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }} sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "20px",
}}
> >
<FormControlLabel
checked={isNonCommercial}
onChange={(e, checked) => setIsNonCommercial(checked)}
control={<Checkbox
sx={{
color: theme.palette.secondary.main,
"&.Mui-checked": {
color: theme.palette.secondary.main,
},
}}
/>}
label="НКО"
sx={{
color: theme.palette.secondary.main,
}}
/>
<Box <Box
sx={{ sx={{
border: "1px solid white", border: "1px solid white",
@ -145,12 +166,14 @@ export default function Cart({ selectedTariffs }: Props) {
} }
}} }}
/> />
{/* <Button
sx={{ maxWidth: "140px" }}
onClick={() => setCoupon(couponField)}
>применить промокод</Button> */}
</Box> </Box>
<Button onClick={handleCalcCartClick}>рассчитать</Button> {cartTotal?.couponState && (
cartTotal.couponState === "applied" ?
<Alert severity="success">Купон применен!</Alert>
:
<Alert severity="error">Подходящий купон не найден!</Alert>
)}
<Button onClick={handleCalcCartClick} sx={{ ml: "auto" }}>рассчитать</Button>
</Paper> </Paper>
{cartTotal?.items && cartTotal.items.length > 0 && {cartTotal?.items && cartTotal.items.length > 0 &&

@ -188,7 +188,7 @@ describe("cart tests", () => {
it("юзер использовал промокод id33. он заменяет скидку на p6 собой. в один момент времени может быть активирован только 1 промокод, т.е. после активации следующего, предыдущий заменяется. но в промокоде может быть несколько скидок. промокоды имеют скидки только на привелеги", () => { it("юзер использовал промокод id33. он заменяет скидку на p6 собой. в один момент времени может быть активирован только 1 промокод, т.е. после активации следующего, предыдущий заменяется. но в промокоде может быть несколько скидок. промокоды имеют скидки только на привелеги", () => {
const testCase = prepareTestCase(exampleCartValues.testCases[9]); const testCase = prepareTestCase(exampleCartValues.testCases[9]);
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts, "ABCD") as CartTotal; const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts, false, "ABCD") as CartTotal;
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)]; const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
cartTotal.items.forEach(cartItem => { cartTotal.items.forEach(cartItem => {
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id)); allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
@ -204,7 +204,7 @@ describe("cart tests", () => {
it("юзер подтвердил свой статус НКО, поэтому, не смотря на то что он достиг по лояльности уровня скидки id2, она не применилась, а применилась id32", () => { it("юзер подтвердил свой статус НКО, поэтому, не смотря на то что он достиг по лояльности уровня скидки id2, она не применилась, а применилась id32", () => {
const testCase = prepareTestCase(exampleCartValues.testCases[10]); const testCase = prepareTestCase(exampleCartValues.testCases[10]);
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal; const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts, true) as CartTotal;
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)]; const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
cartTotal.items.forEach(cartItem => { cartTotal.items.forEach(cartItem => {
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id)); allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));

@ -7,6 +7,7 @@ export function calcCartData(
user: User, user: User,
cartItems: CartItem[], cartItems: CartItem[],
discounts: AnyDiscount[], discounts: AnyDiscount[],
isNonCommercial: boolean = false,
coupon?: string, coupon?: string,
): CartTotal | Error | null { ): CartTotal | Error | null {
let isIncompatibleTariffs = false; let isIncompatibleTariffs = false;
@ -44,11 +45,12 @@ export function calcCartData(
dwarfener: null, dwarfener: null,
}, },
envolvedCartDiscounts: [], envolvedCartDiscounts: [],
couponState: coupon ? "not found" : null,
}; };
// layer 0 // layer 0
for (const discount of discounts) { for (const discount of discounts) {
if (discount.conditionType !== "userType" || discount.condition.userType !== user.Type) continue; if (discount.conditionType !== "userType" || !isNonCommercial) continue;
cartItems.forEach(cartItem => { cartItems.forEach(cartItem => {
cartTotal.items.push({ cartTotal.items.push({
@ -86,6 +88,7 @@ export function calcCartData(
cartItemTotal.totalPrice *= product.factor; cartItemTotal.totalPrice *= product.factor;
cartItemTotal.envolvedDiscounts.push(couponDiscount); cartItemTotal.envolvedDiscounts.push(couponDiscount);
cartTotal.couponState = "applied";
privilegesAffectedByCoupon.push(product.privilegeId); privilegesAffectedByCoupon.push(product.privilegeId);
}); });
@ -123,7 +126,7 @@ export function calcCartData(
} }
// layer 4 // layer 4
const totalPurchasesAmountDiscount = findMaxTotalPurchasesAmountDiscount(discounts, user); const totalPurchasesAmountDiscount = findMaxTotalPurchasesAmountDiscount(discounts, user.PurchasesAmount);
if (totalPurchasesAmountDiscount) { if (totalPurchasesAmountDiscount) {
cartTotal.totalPrice *= totalPurchasesAmountDiscount.factor; cartTotal.totalPrice *= totalPurchasesAmountDiscount.factor;
cartTotal.envolvedCartDiscounts.push(totalPurchasesAmountDiscount); cartTotal.envolvedCartDiscounts.push(totalPurchasesAmountDiscount);
@ -164,9 +167,9 @@ function findMaxCartPurchasesAmountDiscount(discounts: AnyDiscount[], cartTotal:
return maxValueDiscount; return maxValueDiscount;
} }
function findMaxTotalPurchasesAmountDiscount(discounts: AnyDiscount[], user: User): PurchasesAmountDiscount | null { function findMaxTotalPurchasesAmountDiscount(discounts: AnyDiscount[], purchasesAmount: number): PurchasesAmountDiscount | null {
const applicableDiscounts = discounts.filter((discount): discount is PurchasesAmountDiscount => { const applicableDiscounts = discounts.filter((discount): discount is PurchasesAmountDiscount => {
return discount.conditionType === "purchasesAmount" && user.PurchasesAmount >= discount.condition.purchasesAmount; return discount.conditionType === "purchasesAmount" && purchasesAmount >= discount.condition.purchasesAmount;
}); });
if (!applicableDiscounts.length) return null; if (!applicableDiscounts.length) return null;

@ -0,0 +1,26 @@
import { styled } from "@mui/material/styles";
import { Button, Skeleton} from "@mui/material";
const BeautifulButton = styled(Button)(({ theme }) => ({
width: "250px",
margin: "15px auto",
padding: "20px 30px",
fontSize: 18
}));
interface Props {
isReady: boolean
text:string
type?: "button" | "reset" | "submit"
}
export default ({
isReady = true,
text,
type = "button"
}:Props) => {
if (isReady) {
return <BeautifulButton type={type}>{text}</BeautifulButton>
}
return <Skeleton>{text}</Skeleton>
}

65
src/kitUI/makeRequest.ts Normal file

@ -0,0 +1,65 @@
import axios from 'axios'
interface MakeRequest {
method?: string
url: string
body?: unknown
useToken?: boolean
contentType?: boolean
}
export default (props: MakeRequest) => {
return (
new Promise(async (resolve, reject) => {
await makeRequest(props)
.then(r => resolve(r))
.catch(r => reject(r))
})
)
}
function makeRequest({
method = "post",
url,
body,
useToken = true,
contentType = false
}: MakeRequest) {
//В случае 401 рефреш должен попробовать вызваться 1 раз
let counterRefresh = true
let headers: any = {}
if (useToken) headers["Authorization"] = localStorage.getItem('AT')
if (contentType) headers["Content-Type"] = "application/json"
return axios({
url: url,
method: method,
headers: headers,
data: body
})
.then(response => {
if (response.data && response.data.accessToken) {
localStorage.setItem('AT', response.data.accessToken)
}
return response
})
.catch(error => {
if (error.response.status == 401 && counterRefresh) {
refresh().then(response => {
if (response.data && response.data.accessToken) localStorage.setItem('AT', response.data.accessToken)
counterRefresh = false
})
} else {
throw error
}
throw error
})
}
function refresh() {
return axios("https://admin.pena.digital/auth/refresh", {
headers: {
"Authorization": localStorage.getItem('AT'),
"Content-Type": "application/json"
}
})
}

@ -0,0 +1,15 @@
import { styled } from "@mui/material/styles";
import { TextField} from "@mui/material";
export default styled(TextField)(({ theme }) => ({
color: theme.palette.grayLight.main,
"& .MuiInputLabel-root": {
color: theme.palette.grayLight.main,
},
"& .MuiFilledInput-root": {
border: theme.palette.grayLight.main+" 1px solid",
borderRadius: "0",
backgroundColor: theme.palette.hover.main,
color: theme.palette.grayLight.main,
}
}));

@ -0,0 +1,12 @@
import * as React from "react";
import { useLocation, Navigate } from 'react-router-dom'
export default ({ children }: any) => {
const location = useLocation()
//Если пользователь авторизован, перенаправляем его на нужный путь. Иначе выкидываем в регистрацию
if (localStorage.getItem('AT')) {
return children
}
return <Navigate to="/" state={{from: location}} />
}

12
src/kitUI/publicRoute.tsx Normal file

@ -0,0 +1,12 @@
import * as React from "react";
import { useLocation, Navigate } from 'react-router-dom'
export default ({ children }: any) => {
const location = useLocation()
//Если пользователь авторизован, перенаправляем его в приложение. Иначе пускаем куда хотел
if (localStorage.getItem('AT')) {
return <Navigate to="/users" state={{from: location}} />
}
return children
}

@ -140,4 +140,5 @@ export interface CartTotal {
discountsByService: ServiceToDiscountMap; discountsByService: ServiceToDiscountMap;
/** Учтенные скидки типов userType, cartPurchasesAmount, totalPurchasesAmount */ /** Учтенные скидки типов userType, cartPurchasesAmount, totalPurchasesAmount */
envolvedCartDiscounts: (UserTypeDiscount | CartPurchasesAmountDiscount | PurchasesAmountDiscount)[]; envolvedCartDiscounts: (UserTypeDiscount | CartPurchasesAmountDiscount | PurchasesAmountDiscount)[];
couponState: "applied" | "not found" | null;
} }

@ -1,252 +0,0 @@
import * as React from "react";
import { Box, Typography, TextField, Checkbox, Button } from "@mui/material";
import { ThemeProvider } from "@mui/material";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import theme from "../../theme";
import CssBaseline from '@mui/material/CssBaseline';
import Logo from "../Logo";
const Authorization: React.FC = () => {
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.secondary.main,
height: "100%"
}}>
<Box sx={{
width: "100vw",
height: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}>
<Box sx={{
maxWidth: "370px",
height: "700px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
padding: '0 10px'
}}>
<Logo />
<Box sx={{
width: "100%"
}}>
<Typography variant="h5">
Добро пожаловать
</Typography>
<Typography variant="h6" style={{ textAlign: "left" }}>
Мы рады что вы выбрали нас!
</Typography>
</Box>
<Box sx={{
width: "100%",
height: "135px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center"
}}>
<Box sx={{
display: "flex",
width: "100%",
height: "65px"
}}>
<Box sx={{
width: "50px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "left"
}}>
<EmailOutlinedIcon
sx={{ color: theme.palette.golden.main }}
fontSize="large" />
</Box>
<Box sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}>
<TextField
id = "standard-basic"
label = "Эл. почта"
variant = "filled"
color = "secondary"
sx = {{ }}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
paddingRight: '30px'
} }}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
} }}
/>
</Box>
</Box>
<Box sx={{
display: "flex",
width: "100%",
height: "65px",
}}>
<Box sx={{
width: "50px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "left"
}}>
<LockOutlinedIcon
sx={{ color: theme.palette.golden.main }}
fontSize="large" />
</Box>
<Box sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}>
<TextField
id = "outlined-password-input"
label = "Пароль"
type = "password"
variant = "filled"
color = "secondary"
sx = {{ }}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
paddingRight: '30px'
} }}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
} }}
/>
</Box>
</Box>
</Box>
<Box sx={{
width: "100%",
height: "75px"
}}>
<Box sx={{
display: "flex",
width: "100%"
}}>
<Box sx={{
width: "50px",
height: "46px"
}}>
<Checkbox sx={{
color: theme.palette.secondary.main,
transform: "scale(1.5)",
'&.Mui-checked': {
color: theme.palette.secondary.main,
},
}} />
</Box>
<Box sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
fontWeight: "600"
}}>
Запомнить этот компьютер
</Box>
</Box>
<Box sx={{
display: "flex",
width: "100%"
}}>
<Typography
variant = "h4"
sx={{
color: theme.palette.golden.main,
cursor: "pointer",
"&:hover": {
color: theme.palette.goldenDark.main
}
}}>
Забыли пароль?
</Typography>
</Box>
</Box>
<Button
variant = "contained"
sx={{
backgroundColor: theme.palette.content.main,
width: "100%",
padding: '21px 16px',
"&:hover": {
backgroundColor: theme.palette.menu.main
}
}}>
ВОЙТИ
</Button>
<Box sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<Typography
variant = "h4">
У вас нет аккаунта?
</Typography>
<Typography
variant = "h4"
sx={{
color: theme.palette.golden.main,
borderBottom: `1px solid ${theme.palette.golden.main}`,
cursor: "pointer",
"&:hover": {
color: theme.palette.goldenDark.main
}
}}>
Зарегистрируйтесь
</Typography>
</Box>
</Box>
</Box>
</Box>
</ThemeProvider>
</React.Fragment>
);
}
export default Authorization;

@ -0,0 +1,119 @@
import * as React from "react";
import { useNavigate } from "react-router-dom";
import { Formik, Field, Form } from 'formik';
import {useTheme} from "@mui/material/styles";
import { Link } from "react-router-dom"
import {Box, Typography} from "@mui/material";
import Logo from "@pages/Logo";
import CleverButton from "@kitUI/cleverButton"
import MakeRequest from "@kitUI/makeRequest";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import OutlinedInput from "@kitUI/outlinedInput";
export default () => {
const theme = useTheme()
const navigate = useNavigate();
const [restore, setRestore] = React.useState(true)
const [isReady, setIsReady] = React.useState(true)
if (restore) {
return (
<Formik
initialValues={{
mail: ""
}}
onSubmit={(values) => {
setRestore(false)
}}
>
<Form>
<Box component="section"
sx={{
minHeight: "100vh",
height: "100%",
width: "100%",
backgroundColor: theme.palette.content.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "15px 0"
}}
>
<Box component="article"
sx={{
width: "350px",
backgroundColor: theme.palette.content.main,
display: "flex",
flexDirection: "column",
justifyContent: "center",
"> *": {
marginTop: "15px"
}
}}
>
<Typography variant="h6" color={theme.palette.secondary.main}>Восстановление пароля</Typography>
<Logo/>
<Box sx={{display:"flex", alignItems:"center",marginTop:"15px","> *": {marginRight:"10px"}}}>
<EmailOutlinedIcon htmlColor={theme.palette.golden.main}/>
<Field as={OutlinedInput} autoComplete="none" variant="filled" name="mail" label="Эл. почта"/>
</Box>
<CleverButton type="submit" text="Отправить" isReady={isReady}/>
<Link to="/signin" style={{textDecoration:"none"}}><Typography color={theme.palette.golden.main}>Я помню пароль</Typography></Link>
</Box>
</Box>
</Form>
</Formik>
)
} else {
return(
<Formik
initialValues={{
code: ""
}}
onSubmit={(values) => {
}}
>
<Form>
<Box component="section"
sx={{
minHeight: "100vh",
height: "100%",
width: "100%",
backgroundColor: theme.palette.content.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "15px 0"
}}
>
<Box component="article"
sx={{
width: "350px",
backgroundColor: theme.palette.content.main,
display: "flex",
flexDirection: "column",
justifyContent: "center",
"> *": {
marginTop: "15px"
}
}}
>
<Typography variant="h6" color={theme.palette.secondary.main}>Восстановление пароля</Typography>
<Logo/>
<Box sx={{display:"flex", alignItems:"center",marginTop:"15px","> *": {marginRight:"10px"}}}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main}/>
<Field as={OutlinedInput} name="code" variant="filled" label="Код из сообщения"/>
</Box>
<CleverButton type="submit" text="Отправить" isReady={isReady}/>
<Link to="/signin" style={{textDecoration:"none"}}><Typography color={theme.palette.golden.main}>Я помню пароль</Typography></Link>
</Box>
</Box>
</Form>
</Formik>
)
}
}

@ -0,0 +1,137 @@
import * as React from "react"
import { useNavigate } from "react-router-dom";
import { enqueueSnackbar } from 'notistack';
import {useTheme} from "@mui/material/styles"
import { Formik, Field, Form } from 'formik'
import { Link } from "react-router-dom"
import { Box, Checkbox, TextField, Typography, FormControlLabel} from "@mui/material"
import Logo from "@pages/Logo"
import CleverButton from "@kitUI/cleverButton"
import OutlinedInput from "@kitUI/outlinedInput"
import makeRequest from "@kitUI/makeRequest";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined"
interface Values {
email: string;
password: string;
}
function validate(values: Values) {
const errors = {} as any;
if (!values.email) {
errors.email = "Required";
}
if (!values.password) {
errors.password = "Required";
} else if (!/^[\w-]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password";
}
return errors;
}
export default () => {
const theme = useTheme()
const navigate = useNavigate();
const [isReady, setIsReady] = React.useState(true)
return(
<Formik
initialValues={{
email: "",
password: ""
}}
validate={validate}
onSubmit={(values) => {
makeRequest({
url: "https://admin.pena.digital/auth/login",
body: {
"email": values.email,
"password": values.password
},
useToken: false
})
.then((e) => {
console.log(e)
navigate("/users")
})
.catch((e) => {
console.log(e)
enqueueSnackbar(e.message ? e.message : `Unknown error`)
})
}}
>
<Form>
<Box component="section"
sx={{
minHeight: "100vh",
height: "100%",
width: "100%",
backgroundColor: theme.palette.content.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "15px 0"
}}
>
<Box component="article"
sx={{
width: "350px",
backgroundColor: theme.palette.content.main,
display: "flex",
flexDirection: "column",
justifyContent: "center",
"> *": {
marginTop: "15px"
}
}}
>
<Logo/>
<Box>
<Typography variant="h5" color={theme.palette.secondary.main}>Добро пожаловать</Typography>
<Typography variant="h6" color={theme.palette.secondary.main}>Мы рады что вы выбрали нас!</Typography>
</Box>
<Box sx={{display:"flex", alignItems:"center",marginTop:"15px","> *": {marginRight:"10px"}}}>
<EmailOutlinedIcon htmlColor={theme.palette.golden.main}/>
<Field as={OutlinedInput} name="email" variant="filled" label="Эл. почта"/>
</Box>
<Box sx={{display:"flex", alignItems:"center",marginTop:"15px","> *": {marginRight:"10px"}}}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main}/>
<Field as={OutlinedInput} type="password" name="password" variant="filled" label="Пароль"/>
</Box>
<Box component="article"
sx={{
display: "flex",
alignItems: "center"
}}
>
<FormControlLabel
sx={{color:"white"}}
control={<Checkbox
value="checkedA"
inputProps={{ 'aria-label': 'Checkbox A' }}
sx={{
color: "white",
transform: "scale(1.5)",
"&.Mui-checked": {
color: "white",
},
"&.MuiFormControlLabel-root": {
color: "white",
},
}}
/>} label="Запомнить этот компьютер" />
</Box>
<Link to="/restore" style={{textDecoration:"none"}}><Typography color={theme.palette.golden.main}>Забыли пароль?</Typography></Link>
<CleverButton type="submit" text="Войти" isReady={isReady}/>
<Box sx={{
display: "flex"
}}>
<Typography color={theme.palette.secondary.main}>У вас нет аккаунта?&nbsp;</Typography>
<Link to="/signup" style={{textDecoration:"none"}}><Typography color={theme.palette.golden.main}>Зарегестрируйтесь</Typography></Link>
</Box>
</Box>
</Box>
</Form>
</Formik>
)
}

@ -0,0 +1,118 @@
import * as React from "react"
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles"
import { Formik, Field, Form } from "formik"
import { Link } from "react-router-dom"
import {Box, Typography} from "@mui/material"
import CleverButton from "@kitUI/cleverButton"
import OutlinedInput from "@kitUI/outlinedInput";
import makeRequest from "@kitUI/makeRequest";
import Logo from "@pages/Logo/index"
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined"
import LockOutlinedIcon from "@mui/icons-material/LockOutlined"
interface Values {
email: string;
password: string;
repeatPassword: string;
}
function validate(values: Values) {
const errors = {} as any;
if (!values.email) {
errors.login = "Required";
}
if (!values.password) {
errors.password = "Required";
} else if (!/^[\w-]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password";
}
if (values.password !== values.repeatPassword) {
errors.repeatPassword = "Passwords do not match";
}
return errors;
}
export default () => {
const navigate = useNavigate();
const theme = useTheme()
const [isReady, setIsReady] = React.useState(true)
return(
<Formik
initialValues={{
email: "",
password: "",
repeatPassword: ""
}}
validate={validate}
onSubmit={(values) => {
makeRequest({
url: "https://admin.pena.digital/auth/register",
body: {
"login": "login",
"email": values.email,
"password": values.repeatPassword,
"phoneNumber": "+89999999999"
},
useToken: false
})
.then((e) => {
navigate("/users")
})
.catch((e) => {
console.log(e)
enqueueSnackbar(e.response && e.response.data && e.response.data.message ? e.response.data.message : `Unknown error`)
})
}}
>
<Form>
<Box component="section"
sx={{
minHeight: "100vh",
height: "100%",
width: "100%",
backgroundColor: theme.palette.content.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "15px 0"
}}
>
<Box component="article"
sx={{
width: "350px",
backgroundColor: theme.palette.content.main,
display: "flex",
flexDirection: "column",
justifyContent: "center",
"> *": {
marginTop: "15px"
}
}}
>
<Typography variant="h6" color={theme.palette.secondary.main}>Новый аккаунт</Typography>
<Logo/>
<Box>
<Typography variant="h5" color={theme.palette.secondary.main}>Добро пожаловать</Typography>
<Typography variant="h6" color={theme.palette.secondary.main}>Мы рады что вы выбрали нас!</Typography>
</Box>
<Box sx={{display:"flex", alignItems:"center",marginTop:"15px","> *": {marginRight:"10px"}}}>
<EmailOutlinedIcon htmlColor={theme.palette.golden.main}/>
<Field as={OutlinedInput} name="email" variant="filled" label="Эл. почта"/>
</Box>
<Box sx={{display:"flex", alignItems:"center",marginTop:"15px","> *": {marginRight:"10px"}}}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main}/>
<Field as={OutlinedInput} type="password" name="password" variant="filled" label="Пароль"/>
</Box>
<Box sx={{display:"flex", alignItems:"center",marginTop:"15px","> *": {marginRight:"10px"}}}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main}/>
<Field as={OutlinedInput} type="password" name="repeatPassword" variant="filled" label="Повторите пароль"/>
</Box>
<CleverButton type="submit" text="Отправить" isReady={isReady}/>
<Link to="/signin" style={{textDecoration: "none"}} ><Typography color={theme.palette.golden.main}>У меня уже есть аккаунт</Typography></Link>
</Box>
</Box>
</Form>
</Formik>
)
}

@ -1,51 +0,0 @@
import * as React from "react";
import { Box } from "@mui/material";
import Users from "./Users";
import Entities from "./Entities";
import Tariffs from "./Tariffs";
import DiscountManagement from "./DiscountManagement";
import PromocodeManagement from "./PromocodeManagement";
import Support from "./Support";
import Error404 from "../../Error404";
export interface MWProps {
section: number
}
const Content: React.FC<MWProps> = ({ section }) => {
const componentsArray = [
<Error404 />,
<Users />,
<Entities />,
<Tariffs />,
<DiscountManagement />,
<PromocodeManagement />,
<Error404 />,
<Error404 />,
<Support />
];
return (
<React.Fragment>
<Box sx={{
width: "100%",
height: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
overflow: "auto",
overflowY: "auto",
padding: "160px 5px"
}}>
{ componentsArray[ section ] }
</Box>
</React.Fragment>
);
}
export default Content;

@ -1,8 +1,9 @@
import * as React from "react"; import * as React from "react";
import { Box, Typography } from "@mui/material"; import {Box, IconButton, Typography} from "@mui/material";
import theme from "../../../theme"; import theme from "../../../theme";
import ExitToAppOutlinedIcon from '@mui/icons-material/ExitToAppOutlined'; import ExitToAppOutlinedIcon from '@mui/icons-material/ExitToAppOutlined';
import Logo from "../../Logo"; import Logo from "../../Logo";
import makeRequest from "@kitUI/makeRequest";
const Header: React.FC = () => { const Header: React.FC = () => {
@ -43,7 +44,15 @@ const Header: React.FC = () => {
Добро пожаловать, Администратор сервиса Добро пожаловать, Администратор сервиса
</Typography> </Typography>
<Box sx={{ <IconButton
onClick={()=>{
makeRequest({
url: "https://admin.pena.digital/auth/logout",
contentType: true
})
.then(()=>localStorage.setItem('AT', ""))
}}
sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
@ -55,7 +64,7 @@ const Header: React.FC = () => {
color: theme.palette.golden.main, color: theme.palette.golden.main,
transform: "scale(1.3)" transform: "scale(1.3)"
}} /> }} />
</Box> </IconButton>
</Box> </Box>
</Box> </Box>
</React.Fragment> </React.Fragment>

@ -101,15 +101,15 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
}), }),
); );
const links: {path: string; element: JSX.Element; title: string} [] =[ const links: {path: string; element: JSX.Element; title: string, className: string} [] =[
{path: '/users', element: <PersonOutlineOutlinedIcon/>, title: 'Информация о проекте'}, {path: '/users', element: <PersonOutlineOutlinedIcon/>, title: 'Информация о проекте', className:'menu'},
{path: '/entities', element: <SettingsOutlinedIcon/>, title: 'Юридические лица'}, {path: '/entities', element: <SettingsOutlinedIcon/>, title: 'Юридические лица', className:'menu'},
{path: '/tariffs', element: <BathtubOutlinedIcon/>, title: 'Шаблонизатор документов'}, {path: '/tariffs', element: <BathtubOutlinedIcon/>, title: 'Шаблонизатор документов', className:'menu'},
{path: '/discounts', element: <AddPhotoAlternateOutlinedIcon/>, title: 'Скидки'}, {path: '/discounts', element: <AddPhotoAlternateOutlinedIcon/>, title: 'Скидки', className:'menu'},
{path: '/promocode', element: <NaturePeopleOutlinedIcon/>, title: 'Промокод'}, {path: '/promocode', element: <NaturePeopleOutlinedIcon/>, title: 'Промокод', className:'menu'},
{path: '/kkk', element: <SettingsIcon/>, title: 'Настройки'}, {path: '/kkk', element: <SettingsIcon/>, title: 'Настройки', className:'menu'},
{path: '/jjj', element: <CameraIcon/>, title: 'Камера' }, {path: '/jjj', element: <CameraIcon/>, title: 'Камера', className:'menu'},
{path: '/support', element: <HeadsetMicOutlinedIcon/>, title: 'Служба поддержки'}, {path: '/support', element: <HeadsetMicOutlinedIcon/>, title: 'Служба поддержки', className:'menu'},
] ]
@ -122,7 +122,7 @@ const Navigation = (props:any) => {
> >
{links.map((e, i) => ( {links.map((e, i) => (
<ListItem key={i} disablePadding sx={{ display: 'block' }}> <ListItem key={i} disablePadding sx={{ display: 'block' }}>
<Link to={e.path} style={{textDecoration: 'none'}}> <Link to={e.path} style={{textDecoration: 'none'}} className={e.className}>
<ListItemButton onClick={props.SladeMobileHC} <ListItemButton onClick={props.SladeMobileHC}
sx={{ sx={{
minHeight: 48, minHeight: 48,

@ -1,31 +1,26 @@
import * as React from "react"; import * as React from "react";
import { Box } from "@mui/material"; import {Outlet} from 'react-router-dom'
import { ThemeProvider } from "@mui/material"; import {useTheme} from '@mui/material/styles';
import {Box} from "@mui/material";
import {ThemeProvider} from "@mui/material";
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import Menu from "./Menu"; import Menu from "./Menu";
import Header from "./Header"; import Header from "./Header";
import Content from "./Content";
import ModalAdmin from "./ModalAdmin"; import ModalAdmin from "./ModalAdmin";
import ModalUser from "./ModalUser"; import ModalUser from "./ModalUser";
import ModalEntities from "./ModalEntities"; import ModalEntities from "./ModalEntities";
import theme from "../../theme"; import {useMatch} from "react-router-dom";
import { useMatch } from "react-router-dom";
export default () => {
export interface MWProps { const theme = useTheme()
section: number
}
const LoggedIn: React.FC<MWProps> = ({ section }) => {
return ( return (
<React.Fragment> <React.Fragment>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ <Box sx={{
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
height: "100%" height: "100%"
}}> }}
>
<Box sx={{ <Box sx={{
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
display: "flex", display: "flex",
@ -33,7 +28,7 @@ const LoggedIn: React.FC<MWProps> = ({ section }) => {
height: "100%" height: "100%"
}}> }}>
<Menu /> <Menu/>
<Box sx={{ <Box sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
@ -41,8 +36,18 @@ const LoggedIn: React.FC<MWProps> = ({ section }) => {
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center" alignItems: "center"
}}> }}>
{/*<Header />*/} <Box sx={{
<Content section={ section } /> width: "100%",
height: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
overflow: "auto",
overflowY: "auto",
padding: "160px 5px"
}}>
<Outlet/>
</Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -50,11 +55,6 @@ const LoggedIn: React.FC<MWProps> = ({ section }) => {
<ModalAdmin open={useMatch('/modalAdmin') !== null}/> <ModalAdmin open={useMatch('/modalAdmin') !== null}/>
<ModalUser open={useMatch('/modalUser') !== null}/> <ModalUser open={useMatch('/modalUser') !== null}/>
<ModalEntities open={useMatch('/modalEntities') !== null}/> <ModalEntities open={useMatch('/modalEntities') !== null}/>
</ThemeProvider>
</React.Fragment> </React.Fragment>
); )
} }
export default LoggedIn;

@ -28,6 +28,9 @@ declare module '@mui/material/styles' {
content: { content: {
main: string; main: string;
}, },
hover: {
main: string;
},
grayLight: { grayLight: {
main: string; main: string;
}, },
@ -83,7 +86,7 @@ declare module '@mui/material/styles' {
const fontFamily: string = "GilroyRegular"; const fontFamily: string = "GilroyRegular";
const fontWeight: string = "600"; const fontWeight: string = "600";
const options1 = { const paletteColor = {
palette: { palette: {
primary: { primary: {
main: "#111217" main: "#111217"
@ -123,7 +126,7 @@ const options1 = {
} }
}, },
} }
const options2 = { const theme = {
typography: { typography: {
body1: { body1: {
fontFamily: fontFamily fontFamily: fontFamily
@ -169,12 +172,12 @@ const options2 = {
MuiButton: { MuiButton: {
styleOverrides: { styleOverrides: {
root: { root: {
color: options1.palette.secondary.main, color: paletteColor.palette.secondary.main,
backgroundColor: options1.palette.menu.main, backgroundColor: paletteColor.palette.menu.main,
padding: "12px", padding: "12px",
fontSize: "13px", fontSize: "13px",
"&:hover": { "&:hover": {
backgroundColor: options1.palette.hover.main, backgroundColor: paletteColor.palette.hover.main,
} }
} }
}, },
@ -184,11 +187,11 @@ const options2 = {
variant: 'enter' variant: 'enter'
}, },
style: { style: {
color: options1.palette.secondary.main, color: paletteColor.palette.secondary.main,
backgroundColor: options1.palette.content.main, backgroundColor: paletteColor.palette.content.main,
padding: '12px 48px', padding: '12px 48px',
"&:hover": { "&:hover": {
backgroundColor: options1.palette.hover.main, backgroundColor: paletteColor.palette.hover.main,
} }
}, },
@ -202,7 +205,7 @@ const options2 = {
variant: "bar" variant: "bar"
}, },
style: { style: {
backgroundColor: options1.palette.grayMedium.main, backgroundColor: paletteColor.palette.grayMedium.main,
padding: "15px", padding: "15px",
width: "100%" width: "100%"
} }
@ -213,5 +216,4 @@ const options2 = {
}; };
const theme = createTheme(deepmerge(options1, options2)); export default createTheme(deepmerge(paletteColor, theme));
export default theme;

@ -5,7 +5,8 @@
"@theme": ["./theme.ts"], "@theme": ["./theme.ts"],
"@root/*": ["./*"], "@root/*": ["./*"],
"@kitUI/*": ["./kitUI/*"], "@kitUI/*": ["./kitUI/*"],
"@stores/*": ["./stores/*"] "@stores/*": ["./stores/*"],
"@pages/*": ["./pages/*"]
} }
} }
} }

18076
yarn.lock

File diff suppressed because it is too large Load Diff