commit
9bfc5d43c9
17
craco.config.js
Normal file
17
craco.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
const CracoAlias = require("craco-alias");
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
plugin: CracoAlias,
|
||||
options: {
|
||||
source: "tsconfig",
|
||||
// baseUrl SHOULD be specified
|
||||
// plugin does not take it from tsconfig
|
||||
baseUrl: "./src",
|
||||
// tsConfigPath should point to the file where "baseUrl" and "paths" are specified
|
||||
tsConfigPath: "./tsconfig.extend.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
14
package.json
14
package.json
@ -22,8 +22,10 @@
|
||||
"@types/react": "^18.0.18",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"craco": "^0.0.3",
|
||||
"dayjs": "^1.11.5",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^4.0.1",
|
||||
"numeral": "^2.0.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -36,10 +38,11 @@
|
||||
"zustand": "^4.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"test:cart": "craco test src/kitUI/Cart --watchAll=false",
|
||||
"eject": "craco eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@ -58,5 +61,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"craco-alias": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
12
src/__tests__/test.test.js
Normal file
12
src/__tests__/test.test.js
Normal file
@ -0,0 +1,12 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://news.ycombinator.com', {
|
||||
waitUntil: 'networkidle2',
|
||||
});
|
||||
await page.pdf({ path: 'hn.pdf', format: 'a4' });
|
||||
|
||||
await browser.close();
|
||||
})();
|
270
src/kitUI/Cart/Cart.tsx
Normal file
270
src/kitUI/Cart/Cart.tsx
Normal file
@ -0,0 +1,270 @@
|
||||
import theme from "@theme";
|
||||
import { Button, Paper, Box, Typography, TableHead, TableRow, TableCell, TableBody, Table, Tooltip, Alert } from "@mui/material";
|
||||
import Input from "@kitUI/input";
|
||||
import { useCartStore } from "@root/stores/cart";
|
||||
import { useState } from "react";
|
||||
import { GridSelectionModel } from "@mui/x-data-grid";
|
||||
import { testUser } from "@root/stores/mocks/user";
|
||||
import { useDiscountStore } from "@root/stores/discounts";
|
||||
import { calcCartData, createCartItem, findDiscountFactor, formatDiscountFactor } from "./calc";
|
||||
import { useTariffStore } from "@root/stores/tariffs";
|
||||
import { AnyDiscount, CartItemTotal } from "@root/model/cart";
|
||||
|
||||
|
||||
interface Props {
|
||||
selectedTariffs: GridSelectionModel;
|
||||
}
|
||||
|
||||
export default function Cart({ selectedTariffs }: Props) {
|
||||
const tariffs = useTariffStore(store => store.tariffs);
|
||||
const discounts = useDiscountStore(store => store.discounts);
|
||||
const cartTotal = useCartStore(state => state.cartTotal);
|
||||
const setCartTotal = useCartStore(store => store.setCartTotal);
|
||||
const [couponField, setCouponField] = useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
// const [coupon, setCoupon] = useState<string | undefined>();
|
||||
|
||||
const cartRows = cartTotal?.items.map(cartItemTotal => {
|
||||
const service = cartItemTotal.tariff.privilege.serviceKey;
|
||||
const serviceDiscount = cartTotal.discountsByService[service];
|
||||
|
||||
const envolvedDiscountsElement = (
|
||||
<Box>
|
||||
{cartItemTotal.envolvedDiscounts.map((discount, index, arr) => (
|
||||
<span key={discount._id}>
|
||||
<DiscountTooltip
|
||||
discount={discount}
|
||||
cartItemTotal={cartItemTotal}
|
||||
/>
|
||||
{index < arr.length - (serviceDiscount ? 0 : 1) &&
|
||||
<span>, </span>
|
||||
}
|
||||
</span>
|
||||
))}
|
||||
{serviceDiscount &&
|
||||
<span>
|
||||
<DiscountTooltip
|
||||
discount={serviceDiscount}
|
||||
cartItemTotal={cartItemTotal}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const totalIncludingServiceDiscount = cartItemTotal.totalPrice * (serviceDiscount?.target.factor || 1);
|
||||
|
||||
return {
|
||||
id: cartItemTotal.tariff.id,
|
||||
tariffName: cartItemTotal.tariff.name,
|
||||
privilegeDesc: cartItemTotal.tariff.privilege.description,
|
||||
envolvedDiscounts: envolvedDiscountsElement,
|
||||
price: totalIncludingServiceDiscount,
|
||||
};
|
||||
});
|
||||
|
||||
const cartDiscounts = cartTotal?.envolvedCartDiscounts
|
||||
const cartDiscountsResultFactor = cartDiscounts && cartDiscounts.reduce((acc, discount) => acc * findDiscountFactor(discount), 1);
|
||||
|
||||
const envolvedCartDiscountsElement = cartDiscounts && (
|
||||
<Box sx={{
|
||||
display: "inline-flex",
|
||||
flexWrap: "wrap",
|
||||
}}>
|
||||
{cartDiscounts?.map((discount, index, arr) => (
|
||||
<span key={discount._id}>
|
||||
<DiscountTooltip
|
||||
discount={discount}
|
||||
/>
|
||||
{index < arr.length - 1 &&
|
||||
<span>, </span>
|
||||
}
|
||||
</span>
|
||||
))}
|
||||
|
||||
{cartDiscountsResultFactor && `= ${formatDiscountFactor(cartDiscountsResultFactor)}`}
|
||||
</Box>
|
||||
);
|
||||
|
||||
function handleCalcCartClick() {
|
||||
const cartTariffs = tariffs.filter(tariff => selectedTariffs.includes(tariff.id));
|
||||
const cartItems = cartTariffs.map(tariff => createCartItem(tariff));
|
||||
const cartData = calcCartData(testUser, cartItems, discounts, couponField);
|
||||
|
||||
if (cartData instanceof Error) {
|
||||
setErrorMessage(cartData.message);
|
||||
return setCartTotal(null);
|
||||
}
|
||||
|
||||
setErrorMessage(null);
|
||||
setCartTotal(cartData);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="section"
|
||||
sx={{
|
||||
border: "1px solid white",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
pb: "20px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
корзина
|
||||
</Typography>
|
||||
<Paper
|
||||
variant="bar"
|
||||
sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
border: "1px solid white",
|
||||
padding: "3px",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
label="промокод"
|
||||
size="small"
|
||||
value={couponField}
|
||||
onChange={e => setCouponField(e.target.value)}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* <Button
|
||||
sx={{ maxWidth: "140px" }}
|
||||
onClick={() => setCoupon(couponField)}
|
||||
>применить промокод</Button> */}
|
||||
</Box>
|
||||
<Button onClick={handleCalcCartClick}>рассчитать</Button>
|
||||
</Paper>
|
||||
|
||||
{cartTotal?.items && cartTotal.items.length > 0 &&
|
||||
<>
|
||||
<Table sx={{
|
||||
width: "90%",
|
||||
margin: "5px",
|
||||
border: "2px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
}} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow sx={{
|
||||
borderBottom: "2px solid",
|
||||
borderColor: theme.palette.grayLight.main,
|
||||
height: "100px"
|
||||
}}>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ color: theme.palette.secondary.main }}
|
||||
>Имя</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ color: theme.palette.secondary.main }}
|
||||
>Описание</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ color: theme.palette.secondary.main }}
|
||||
>Скидки</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ color: theme.palette.secondary.main }}
|
||||
>стоимость</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{cartRows?.map(row => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
sx={{
|
||||
borderBottom: "2px solid",
|
||||
borderColor: theme.palette.grayLight.main,
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
}}>{row.tariffName}</TableCell>
|
||||
<TableCell sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
}}>{row.privilegeDesc}</TableCell>
|
||||
<TableCell sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
}}>{row.envolvedDiscounts}</TableCell>
|
||||
<TableCell sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
}}>{row.price.toFixed(2)} ₽</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Typography id="transition-modal-title" variant="h6" sx={{
|
||||
fontWeight: "normal",
|
||||
textAlign: "center",
|
||||
marginTop: "15px",
|
||||
fontSize: "16px"
|
||||
}}>
|
||||
Скидки корзины: {envolvedCartDiscountsElement}
|
||||
</Typography>
|
||||
|
||||
<Typography id="transition-modal-title" variant="h6" sx={{
|
||||
fontWeight: "normal",
|
||||
textAlign: "center",
|
||||
marginTop: "10px"
|
||||
}}>
|
||||
ИТОГО: <span>{cartTotal?.totalPrice.toFixed(2)} ₽</span>
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
|
||||
{errorMessage !== null &&
|
||||
<Alert variant="filled" severity="error" sx={{ mt: "20px" }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function DiscountTooltip({ discount, cartItemTotal }: {
|
||||
discount: AnyDiscount;
|
||||
cartItemTotal?: CartItemTotal;
|
||||
}) {
|
||||
const discountText = formatDiscountFactor(findDiscountFactor(discount, cartItemTotal?.tariff.privilege.privilegeId));
|
||||
|
||||
return (
|
||||
<Tooltip title={
|
||||
<>
|
||||
<Typography>Скидка: {discount?.name}</Typography>
|
||||
<Typography>{discount?.description}</Typography>
|
||||
<Typography>-------</Typography>
|
||||
<Typography>layer: {discount?.layer}</Typography>
|
||||
<Typography>id: {discount?._id}</Typography>
|
||||
</>
|
||||
}>
|
||||
<span>{discountText}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
265
src/kitUI/Cart/calc.test.ts
Normal file
265
src/kitUI/Cart/calc.test.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { CartItem, CartTotal } from "../../model/cart";
|
||||
import { SERVICE_LIST, Tariff } from "../../model/tariff";
|
||||
import { User } from "../../model/user";
|
||||
import { exampleCartValues, TestCase } from "../../stores/mocks/exampleCartValues";
|
||||
import { calcCartData, createCartItem } from "./calc";
|
||||
|
||||
|
||||
const MAX_PRICE_ERROR = 0.01;
|
||||
const discounts = exampleCartValues.discounts;
|
||||
|
||||
describe("cart tests", () => {
|
||||
it("без скидок", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[0]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("сумма в корзине достигла 5к, поэтому применилась скидка", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[1]);
|
||||
|
||||
// работает если не учитывать скидки id26, id27 (скидка на templategen от 1000/5000 р.)
|
||||
const discountsWithoutTemplategen = discounts.filter(discount => {
|
||||
return !(
|
||||
discount.conditionType === "service" &&
|
||||
discount.condition.service.id === "templategen"
|
||||
);
|
||||
});
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discountsWithoutTemplategen) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("добавил кастомный тариф такой, чтобы пофвилась скидка на продукт", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[2]);
|
||||
|
||||
// работает если не учитывать скидки id26, id27 (скидка на templategen от 1000/5000 р.)
|
||||
const discountsWithoutTemplategen = discounts.filter(discount => {
|
||||
return !(
|
||||
discount.conditionType === "service" &&
|
||||
discount.condition.service.id === "templategen"
|
||||
);
|
||||
});
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discountsWithoutTemplategen) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("т.е. применилась не id14, а id15, потому что применяется наибольшая подходящая. в то же время, на скидку за лояльность ещё не хватает", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[3]);
|
||||
|
||||
// работает если не учитывать скидки id26, id27 (скидка на templategen от 1000/5000 р.)
|
||||
const discountsWithoutTemplategen = discounts.filter(discount => {
|
||||
return !(
|
||||
discount.conditionType === "service" &&
|
||||
discount.condition.service.id === "templategen"
|
||||
);
|
||||
});
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discountsWithoutTemplategen) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("case 5", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[4]);
|
||||
|
||||
// работает если не учитывать скидки id26, id27 (скидка на templategen от 1000/5000 р.)
|
||||
const discountsWithoutTemplategen = discounts.filter(discount => {
|
||||
return !(
|
||||
discount.conditionType === "service" &&
|
||||
discount.condition.service.id === "templategen"
|
||||
);
|
||||
});
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discountsWithoutTemplategen) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("история про то, как скидки за привилегии помешали получить скидку за сервис", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[5]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("то же что и выше, но без лояльности", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[6]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("история про то, как получилось получить скидку за сервис", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[7]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("две скидки за сервис", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[8]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("юзер использовал промокод id33. он заменяет скидку на p6 собой. в один момент времени может быть активирован только 1 промокод, т.е. после активации следующего, предыдущий заменяется. но в промокоде может быть несколько скидок. промокоды имеют скидки только на привелеги", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[9]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts, "ABCD") as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("юзер подтвердил свой статус НКО, поэтому, не смотря на то что он достиг по лояльности уровня скидки id2, она не применилась, а применилась id32", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[10]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
it("case 12", () => {
|
||||
const testCase = prepareTestCase(exampleCartValues.testCases[11]);
|
||||
|
||||
const cartTotal = calcCartData(testCase.user, testCase.cartItems, discounts) as CartTotal;
|
||||
const allEnvolvedDiscounts: string[] = [...cartTotal.envolvedCartDiscounts.map(discount => discount._id)];
|
||||
cartTotal.items.forEach(cartItem => {
|
||||
allEnvolvedDiscounts.push(...cartItem.envolvedDiscounts.map(discount => discount._id));
|
||||
});
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
if (cartTotal.discountsByService[service]) allEnvolvedDiscounts.push(cartTotal.discountsByService[service]!._id);
|
||||
});
|
||||
|
||||
expect(allEnvolvedDiscounts.sort()).toEqual(testCase.expect.envolvedDiscounts.sort());
|
||||
expect(cartTotal.totalPrice).toBeGreaterThan(testCase.expect.price - MAX_PRICE_ERROR);
|
||||
expect(cartTotal.totalPrice).toBeLessThan(testCase.expect.price + MAX_PRICE_ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function prepareTestCase(testCase: TestCase): ({
|
||||
expect: {
|
||||
price: number;
|
||||
envolvedDiscounts: string[];
|
||||
};
|
||||
user: User;
|
||||
cartItems: CartItem[];
|
||||
}) {
|
||||
const user = testCase.input.UserInformation;
|
||||
const tariffs = testCase.input.Products.map((testProduct): Tariff => ({
|
||||
id: "someId",
|
||||
name: "someName",
|
||||
amount: testProduct.Amount,
|
||||
customPricePerUnit: testProduct.Price && testProduct.Price / testProduct.Amount, // приводим price из сниппета к pricePerUnit
|
||||
privilege: findPrivilegeById(testProduct.ID)
|
||||
}));
|
||||
const cartItems: CartItem[] = tariffs.map(createCartItem);
|
||||
|
||||
return { expect: testCase.expect, user, cartItems };
|
||||
}
|
||||
|
||||
function findPrivilegeById(id: string) {
|
||||
const privilege = exampleCartValues.privileges.find(privilege => privilege.privilegeId === id);
|
||||
if (!privilege) throw new Error(`Privilege not found with id ${id}`);
|
||||
|
||||
return privilege;
|
||||
}
|
249
src/kitUI/Cart/calc.ts
Normal file
249
src/kitUI/Cart/calc.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import { CartItem, AnyDiscount, CartTotal, CartItemTotal, PrivilegeDiscount, CartPurchasesAmountDiscount, PurchasesAmountDiscount, ServiceToPriceMap, ServiceDiscount, UserDiscount } from "@root/model/cart";
|
||||
import { ServiceType, SERVICE_LIST, Tariff } from "../../model/tariff";
|
||||
import { User } from "../../model/user";
|
||||
|
||||
|
||||
export function calcCartData(
|
||||
user: User,
|
||||
cartItems: CartItem[],
|
||||
discounts: AnyDiscount[],
|
||||
coupon?: string,
|
||||
): CartTotal | Error | null {
|
||||
let isIncompatibleTariffs = false;
|
||||
|
||||
const defaultTariffTypePresent: { [Key in ServiceType]: boolean } = {
|
||||
dwarfener: false,
|
||||
squiz: false,
|
||||
templategen: false,
|
||||
};
|
||||
|
||||
cartItems.forEach(cartItem => {
|
||||
if (cartItem.tariff.customPricePerUnit === undefined) return defaultTariffTypePresent[cartItem.tariff.privilege.serviceKey] = true;
|
||||
|
||||
if (
|
||||
defaultTariffTypePresent[cartItem.tariff.privilege.serviceKey] &&
|
||||
cartItem.tariff.customPricePerUnit !== undefined
|
||||
) isIncompatibleTariffs = true;
|
||||
});
|
||||
|
||||
if (isIncompatibleTariffs) return new Error("Если взят готовый тариф, то кастомный на этот сервис сделать уже нельзя");
|
||||
|
||||
if (!cartItems.length) return null;
|
||||
|
||||
const cartTotal: CartTotal = {
|
||||
items: [],
|
||||
totalPrice: 0,
|
||||
priceByService: {
|
||||
templategen: 0,
|
||||
squiz: 0,
|
||||
dwarfener: 0,
|
||||
},
|
||||
discountsByService: {
|
||||
templategen: null,
|
||||
squiz: null,
|
||||
dwarfener: null,
|
||||
},
|
||||
envolvedCartDiscounts: [],
|
||||
};
|
||||
|
||||
// layer 0
|
||||
for (const discount of discounts) {
|
||||
if (discount.conditionType !== "userType" || discount.condition.userType !== user.Type) continue;
|
||||
|
||||
cartItems.forEach(cartItem => {
|
||||
cartTotal.items.push({
|
||||
envolvedDiscounts: [],
|
||||
tariff: cartItem.tariff,
|
||||
totalPrice: cartItem.price,
|
||||
});
|
||||
|
||||
cartTotal.priceByService[cartItem.tariff.privilege.serviceKey] += cartItem.price;
|
||||
cartTotal.totalPrice += cartItem.price;
|
||||
});
|
||||
|
||||
cartTotal.totalPrice *= discount.target.factor;
|
||||
cartTotal.envolvedCartDiscounts.push(discount);
|
||||
|
||||
return cartTotal;
|
||||
}
|
||||
|
||||
const couponDiscount = coupon ? findUserDiscount(discounts, user, coupon) : null;
|
||||
|
||||
// layer 1
|
||||
for (const cartItem of cartItems) {
|
||||
const cartItemTotal: CartItemTotal = {
|
||||
tariff: cartItem.tariff,
|
||||
envolvedDiscounts: [],
|
||||
totalPrice: cartItem.price,
|
||||
};
|
||||
|
||||
const tariff = cartItem.tariff;
|
||||
const privilegesAffectedByCoupon: string[] = [];
|
||||
|
||||
couponDiscount?.target.products.forEach(product => {
|
||||
if (product.privilegeId !== tariff.privilege.privilegeId) return;
|
||||
if (tariff.customPricePerUnit !== undefined && !couponDiscount.overwhelm) return;
|
||||
|
||||
cartItemTotal.totalPrice *= product.factor;
|
||||
cartItemTotal.envolvedDiscounts.push(couponDiscount);
|
||||
privilegesAffectedByCoupon.push(product.privilegeId);
|
||||
});
|
||||
|
||||
const privilegeDiscount = findMaxApplicablePrivilegeDiscount(discounts, tariff);
|
||||
|
||||
privilegeDiscount?.target.products.forEach(product => {
|
||||
if (product.privilegeId !== tariff.privilege.privilegeId) return;
|
||||
if (tariff.customPricePerUnit !== undefined) return;
|
||||
if (privilegesAffectedByCoupon.includes(privilegeDiscount.condition.privilege.id)) return;
|
||||
|
||||
cartItemTotal.totalPrice *= product.factor;
|
||||
cartItemTotal.envolvedDiscounts.push(privilegeDiscount);
|
||||
});
|
||||
|
||||
cartTotal.items.push(cartItemTotal);
|
||||
cartTotal.priceByService[tariff.privilege.serviceKey] += cartItemTotal.totalPrice;
|
||||
}
|
||||
|
||||
// layer 2
|
||||
SERVICE_LIST.map(service => service.serviceKey).forEach(service => {
|
||||
const serviceDiscount = findMaxServiceDiscount(service, discounts, cartTotal.priceByService);
|
||||
if (serviceDiscount) {
|
||||
cartTotal.priceByService[service] *= serviceDiscount.target.factor;
|
||||
cartTotal.discountsByService[service] = serviceDiscount;
|
||||
}
|
||||
|
||||
cartTotal.totalPrice += cartTotal.priceByService[service];
|
||||
});
|
||||
|
||||
// layer 3
|
||||
const cartPurchasesAmountDiscount = findMaxCartPurchasesAmountDiscount(discounts, cartTotal);
|
||||
if (cartPurchasesAmountDiscount) {
|
||||
cartTotal.totalPrice *= cartPurchasesAmountDiscount.factor;
|
||||
cartTotal.envolvedCartDiscounts.push(cartPurchasesAmountDiscount);
|
||||
}
|
||||
|
||||
// layer 4
|
||||
const totalPurchasesAmountDiscount = findMaxTotalPurchasesAmountDiscount(discounts, user);
|
||||
if (totalPurchasesAmountDiscount) {
|
||||
cartTotal.totalPrice *= totalPurchasesAmountDiscount.factor;
|
||||
cartTotal.envolvedCartDiscounts.push(totalPurchasesAmountDiscount);
|
||||
}
|
||||
|
||||
return cartTotal;
|
||||
}
|
||||
|
||||
function findMaxApplicablePrivilegeDiscount(discounts: AnyDiscount[], tariff: Tariff): PrivilegeDiscount | null {
|
||||
const applicableDiscounts = discounts.filter((discount): discount is PrivilegeDiscount => {
|
||||
return (
|
||||
discount.conditionType === "privilege" &&
|
||||
tariff.privilege.privilegeId === discount.condition.privilege.id &&
|
||||
tariff.amount >= discount.condition.privilege.value
|
||||
);
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce(
|
||||
(prev, current) => current.condition.privilege.value > prev.condition.privilege.value ? current : prev
|
||||
);
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
function findMaxCartPurchasesAmountDiscount(discounts: AnyDiscount[], cartTotal: CartTotal): CartPurchasesAmountDiscount | null {
|
||||
const applicableDiscounts = discounts.filter((discount): discount is CartPurchasesAmountDiscount => {
|
||||
return discount.conditionType === "cartPurchasesAmount" && cartTotal.totalPrice >= discount.condition.cartPurchasesAmount;
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce(
|
||||
(prev, current) => current.condition.cartPurchasesAmount > prev.condition.cartPurchasesAmount ? current : prev
|
||||
);
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
function findMaxTotalPurchasesAmountDiscount(discounts: AnyDiscount[], user: User): PurchasesAmountDiscount | null {
|
||||
const applicableDiscounts = discounts.filter((discount): discount is PurchasesAmountDiscount => {
|
||||
return discount.conditionType === "purchasesAmount" && user.PurchasesAmount >= discount.condition.purchasesAmount;
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce(
|
||||
(prev, current) => current.condition.purchasesAmount > prev.condition.purchasesAmount ? current : prev
|
||||
);
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
function findMaxServiceDiscount(
|
||||
service: ServiceType,
|
||||
discounts: AnyDiscount[],
|
||||
priceByService: ServiceToPriceMap,
|
||||
): ServiceDiscount | null {
|
||||
const discountsForTariffService = discounts.filter((discount): discount is ServiceDiscount => {
|
||||
return (
|
||||
discount.conditionType === "service" &&
|
||||
discount.condition.service.id === service &&
|
||||
priceByService[service] >= discount.condition.service.value
|
||||
);
|
||||
});
|
||||
|
||||
if (!discountsForTariffService.length) return null;
|
||||
|
||||
const maxValueDiscount = discountsForTariffService.reduce((prev, current) => {
|
||||
return current.condition.service.value > prev.condition.service.value ? current : prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
function findUserDiscount(discounts: AnyDiscount[], user: User, coupon: string,): UserDiscount | null {
|
||||
const userDiscount = discounts.find((discount): discount is UserDiscount => {
|
||||
return (
|
||||
discount.conditionType === "user" &&
|
||||
discount.condition.user === user.ID &&
|
||||
discount.condition.coupon === coupon
|
||||
);
|
||||
});
|
||||
|
||||
return userDiscount ?? null;
|
||||
}
|
||||
|
||||
export function createCartItem(tariff: Tariff): CartItem {
|
||||
const pricePerUnit = tariff.customPricePerUnit ?? tariff.privilege.pricePerUnit;
|
||||
const price = pricePerUnit * tariff.amount;
|
||||
|
||||
return { tariff, price, id: "someId" };
|
||||
}
|
||||
|
||||
export function findDiscountFactor(discount: AnyDiscount, privilegeId?: string) {
|
||||
switch (discount.conditionType) {
|
||||
case "cartPurchasesAmount":
|
||||
return discount.factor;
|
||||
case "purchasesAmount":
|
||||
return discount.factor;
|
||||
case "privilege": {
|
||||
const product = discount.target.products.find(product => product.privilegeId === privilegeId);
|
||||
if (!product) throw new Error("Discount target product not found");
|
||||
|
||||
return product.factor;
|
||||
}
|
||||
case "user": {
|
||||
const product = discount.target.products.find(product => product.privilegeId === privilegeId);
|
||||
if (!product) throw new Error("Discount target product not found");
|
||||
|
||||
return product.factor;
|
||||
}
|
||||
case "service":
|
||||
return discount.target.factor;
|
||||
case "userType":
|
||||
return discount.target.factor;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDiscountFactor(factor: number): string {
|
||||
return `${((1 - factor) * 100).toFixed(1)}%`;
|
||||
}
|
26
src/kitUI/datagrid.tsx
Normal file
26
src/kitUI/datagrid.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { DataGrid } from "@mui/x-data-grid";
|
||||
import { styled } from "@mui/material/styles";
|
||||
export default styled(DataGrid)(({ theme }) => ({
|
||||
width: "100%",
|
||||
minHeight: "400px",
|
||||
margin: "10px 0",
|
||||
color: theme.palette.secondary.main,
|
||||
"& .MuiDataGrid-iconSeparator": {
|
||||
display: "none"
|
||||
},
|
||||
"& .css-levciy-MuiTablePagination-displayedRows": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiTablePagination-selectLabel": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiButton-text": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
}));
|
16
src/kitUI/input.tsx
Normal file
16
src/kitUI/input.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import {TextField} from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
export default styled(TextField)(({ theme }) => ({
|
||||
variant: "outlined",
|
||||
height: "40px",
|
||||
size: "small",
|
||||
color: theme.palette.secondary.main,
|
||||
width: "140px",
|
||||
backgroundColor: theme.palette.content.main,
|
||||
"& .MuiFormLabel-root": {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
"& .Mui-focused": {
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}));
|
@ -1,36 +1,143 @@
|
||||
import { ServiceType } from "./tariff";
|
||||
import { ServiceType, Privilege, Tariff } from "./tariff";
|
||||
|
||||
|
||||
export interface CartSummary {
|
||||
mbs: number;
|
||||
points: number;
|
||||
days: number;
|
||||
interface DiscountBase {
|
||||
_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** Этап применения скидки */
|
||||
layer: number;
|
||||
}
|
||||
|
||||
export interface PurchasesAmountDiscount extends DiscountBase {
|
||||
conditionType: "purchasesAmount";
|
||||
condition: {
|
||||
purchasesAmount: number;
|
||||
};
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}
|
||||
|
||||
export interface CartPurchasesAmountDiscount extends DiscountBase {
|
||||
conditionType: "cartPurchasesAmount";
|
||||
condition: {
|
||||
cartPurchasesAmount: number;
|
||||
};
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}
|
||||
|
||||
export interface PrivilegeDiscount extends DiscountBase {
|
||||
conditionType: "privilege";
|
||||
condition: {
|
||||
privilege: {
|
||||
id: string;
|
||||
/** Скидка применяется, если значение больше или равно этому значению */
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
target: {
|
||||
products: Array<{
|
||||
privilegeId: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServiceDiscount extends DiscountBase {
|
||||
conditionType: "service";
|
||||
condition: {
|
||||
service: {
|
||||
id: ServiceType;
|
||||
/** Скидка применяется, если значение больше или равно этому значению */
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
target: {
|
||||
service: ServiceType;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserTypeDiscount extends DiscountBase {
|
||||
conditionType: "userType";
|
||||
condition: {
|
||||
userType: string;
|
||||
};
|
||||
target: {
|
||||
IsAllProducts: boolean;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
};
|
||||
overwhelm: boolean;
|
||||
}
|
||||
|
||||
export interface UserDiscount extends DiscountBase {
|
||||
conditionType: "user";
|
||||
condition: {
|
||||
coupon: string;
|
||||
user: string;
|
||||
};
|
||||
target: {
|
||||
products: Array<{
|
||||
privilegeId: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}>;
|
||||
};
|
||||
overwhelm: boolean;
|
||||
}
|
||||
|
||||
export type AnyDiscount =
|
||||
| PurchasesAmountDiscount
|
||||
| CartPurchasesAmountDiscount
|
||||
| PrivilegeDiscount
|
||||
| ServiceDiscount
|
||||
| UserTypeDiscount
|
||||
| UserDiscount;
|
||||
|
||||
export type DiscountConditionType = AnyDiscount["conditionType"];
|
||||
|
||||
export interface Promocode {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
endless: boolean;
|
||||
from: string;
|
||||
dueTo: string;
|
||||
privileges: Array<Privileges>;
|
||||
privileges: Privilege[];
|
||||
}
|
||||
|
||||
export interface Privileges {
|
||||
good: ServiceType,
|
||||
discount: number;
|
||||
export interface CartItem {
|
||||
id: string;
|
||||
tariff: Tariff;
|
||||
/** Посчитанная цена пункта корзины */
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface Discount {
|
||||
id: number;
|
||||
name: string;
|
||||
endless: boolean;
|
||||
from: string;
|
||||
dueTo: string;
|
||||
privileges: Array<Privileges>;
|
||||
active: boolean;
|
||||
basketMore: number;
|
||||
incomeMore: number;
|
||||
toTime: number;
|
||||
toCapacity: number;
|
||||
/** Пункт корзины с уже примененными скидками */
|
||||
export interface CartItemTotal {
|
||||
/** Массив с id примененных скидок */
|
||||
envolvedDiscounts: (PrivilegeDiscount | UserDiscount)[];
|
||||
totalPrice: number;
|
||||
tariff: Tariff;
|
||||
}
|
||||
|
||||
export type ServiceToPriceMap = {
|
||||
[Key in ServiceType]: number;
|
||||
};
|
||||
|
||||
export type ServiceToDiscountMap = {
|
||||
[Key in ServiceType]: ServiceDiscount | null;
|
||||
};
|
||||
|
||||
export interface CartTotal {
|
||||
items: CartItemTotal[];
|
||||
totalPrice: number;
|
||||
priceByService: ServiceToPriceMap;
|
||||
/** Скидки по сервисам */
|
||||
discountsByService: ServiceToDiscountMap;
|
||||
/** Учтенные скидки типов userType, cartPurchasesAmount, totalPurchasesAmount */
|
||||
envolvedCartDiscounts: (UserTypeDiscount | CartPurchasesAmountDiscount | PurchasesAmountDiscount)[];
|
||||
}
|
@ -1,43 +1,46 @@
|
||||
export type ServiceType =
|
||||
| "Шаблонизатор документов"
|
||||
| "Опросник"
|
||||
| "Сокращатель ссылок"
|
||||
| "АБ тесты";
|
||||
|
||||
|
||||
export const SERVICE_LIST = [
|
||||
{
|
||||
serviceKey: "templategen",
|
||||
displayName: "Шаблонизатор документов"
|
||||
},
|
||||
{
|
||||
serviceKey: "squiz",
|
||||
displayName: "Опросник"
|
||||
},
|
||||
{
|
||||
serviceKey: "dwarfener",
|
||||
displayName: "Сокращатель ссылок"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export type ServiceType = typeof SERVICE_LIST[number]["serviceKey"];
|
||||
|
||||
export type PrivilegeType =
|
||||
| "unlim"
|
||||
| "gencount"
|
||||
| "activequiz"
|
||||
| "abcount"
|
||||
| "extended";
|
||||
|
||||
export interface Privilege {
|
||||
serviceKey: ServiceType;
|
||||
name: PrivilegeType;
|
||||
privilegeId: string;
|
||||
description: string;
|
||||
/** Единица измерения привелегии: время в днях/кол-во */
|
||||
type: "day" | "count";
|
||||
/** Стоимость одной единицы привелегии */
|
||||
pricePerUnit: number;
|
||||
}
|
||||
|
||||
export interface Tariff {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
service: ServiceType | "";
|
||||
disk: number;
|
||||
time: number;
|
||||
points: number;
|
||||
price: number;
|
||||
privilege: Privilege;
|
||||
/** Количество единиц привелегии */
|
||||
amount: number;
|
||||
/** Кастомная цена, если есть, то используется вместо privilege.price */
|
||||
customPricePerUnit?: number;
|
||||
}
|
||||
|
||||
// TODO тип пакета тарифов надо как-то реорганизовать
|
||||
export interface ArrayProps {
|
||||
id: number;
|
||||
name: string;
|
||||
type: "package" | "tariff";
|
||||
service: ServiceType | "";
|
||||
disk: number;
|
||||
time: number;
|
||||
points: number;
|
||||
price: number;
|
||||
tariffs?: Array<Tariff>;
|
||||
}
|
||||
|
||||
// Идея для типа пакета тарифов
|
||||
interface TariffPackage {
|
||||
id: number;
|
||||
name: string;
|
||||
service: ServiceType;
|
||||
disk: number;
|
||||
time: number;
|
||||
points: number;
|
||||
price: number;
|
||||
tariffs: Tariff[];
|
||||
}
|
||||
|
||||
type TariffsOrPackages = Array<Tariff | TariffPackage>; // Этот тип должен пойти вместо ArrayProps
|
5
src/model/user.ts
Normal file
5
src/model/user.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface User {
|
||||
ID: string;
|
||||
Type: "" | "nko";
|
||||
PurchasesAmount: number;
|
||||
}
|
605
src/pages/dashboard/Content/DiscountManagement.tsx
Normal file
605
src/pages/dashboard/Content/DiscountManagement.tsx
Normal file
@ -0,0 +1,605 @@
|
||||
import { Box, Typography, TextField, Checkbox, Button } from "@mui/material";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { DataGrid, GridColDef, GridRowsProp, GridToolbar } from "@mui/x-data-grid";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
import theme from "../../../theme";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { useDiscountStore } from "../../../stores/discounts";
|
||||
import { useState } from "react";
|
||||
import { DiscountConditionType } from "@root/model/cart";
|
||||
import { ServiceType } from "@root/model/tariff";
|
||||
|
||||
|
||||
const BoxButton = styled('div')(({ theme }) => ({
|
||||
[theme.breakpoints.down(400)]: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
// {
|
||||
// field: "endless",
|
||||
// headerName: "Бесконечная",
|
||||
// width: 120,
|
||||
// sortable: false,
|
||||
// },
|
||||
// {
|
||||
// field: "from",
|
||||
// headerName: "От",
|
||||
// width: 120,
|
||||
// sortable: false,
|
||||
// },
|
||||
// {
|
||||
// field: "dueTo",
|
||||
// headerName: "До",
|
||||
// width: 120,
|
||||
// sortable: false,
|
||||
// },
|
||||
// {
|
||||
// field: "privileges",
|
||||
// headerName: "Привилегии",
|
||||
// width: 210,
|
||||
// sortable: false,
|
||||
// },
|
||||
// {
|
||||
// field: "active",
|
||||
// headerName: "Активна",
|
||||
// width: 100,
|
||||
// sortable: false,
|
||||
// },
|
||||
// {
|
||||
// field: "basketMore",
|
||||
// headerName: "Корзина больше",
|
||||
// width: 140,
|
||||
// sortable: false,
|
||||
// }
|
||||
// ,
|
||||
// {
|
||||
// field: "toTime",
|
||||
// headerName: "На время",
|
||||
// width: 140,
|
||||
// sortable: false,
|
||||
// }
|
||||
// ,
|
||||
// {
|
||||
// field: "toCapacity",
|
||||
// headerName: "На объем",
|
||||
// width: 140,
|
||||
// sortable: false,
|
||||
// },
|
||||
{
|
||||
field: "id",
|
||||
headerName: "ID",
|
||||
width: 30,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название скидки",
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
headerName: "Описание",
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "conditionType",
|
||||
headerName: "Тип условия",
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const DiscountManagement: React.FC = () => {
|
||||
const discounts = useDiscountStore(state => state.discounts);
|
||||
const [isInfinite, setIsInfinite] = useState<boolean>(false);
|
||||
const [serviceType, setServiceType] = useState<ServiceType>("templategen");
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndDate] = useState<Date>(new Date());
|
||||
const [discountTypeField, setDiscountTypeField] = useState<string>("");
|
||||
const [discountNameField, setDiscountNameField] = useState<string>("");
|
||||
const [discountDescriptionField, setDiscountDescriptionField] = useState<string>("");
|
||||
const [discountConditionType, setDiscountConditionType] = useState<DiscountConditionType | null>(null);
|
||||
const [discountFactor, setDiscountFactor] = useState<number>(1);
|
||||
const [purchasesAmountThreshold, setPurchasesAmountThreshold] = useState<number>(0);
|
||||
const [cartPurchasesAmountThreshold, setCartPurchasesAmountThreshold] = useState<number>(0);
|
||||
const [discountMinValue, setDiscountMinValue] = useState<number>(0);
|
||||
|
||||
const handleServiceTypeChange = (event: SelectChangeEvent) => {
|
||||
setServiceType(event.target.value as ServiceType);
|
||||
};
|
||||
|
||||
function createDiscount() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function activateDiscounts() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function deactivateDiscounts() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// const discountsArrayConverted = discounts.map((item) => {
|
||||
// const basketMorePerc = Math.round(Number(item.basketMore) * 100) + "%";
|
||||
// const toTimePerc = Math.round(Number(item.toTime) * 100) + "%";
|
||||
// const toCapacityPerc = Math.round(Number(item.toCapacity) * 100) + "%";
|
||||
|
||||
// const dateFrom = item.from ? new Date(Number(item.from)) : "";
|
||||
// const dateDueTo = item.from ? new Date(Number(item.dueTo)) : "";
|
||||
|
||||
// const strFrom = dateFrom
|
||||
// ? `${dateFrom.getDate()}.${dateFrom.getMonth()}.${dateFrom.getFullYear()}`
|
||||
// : "-";
|
||||
|
||||
// const strDueTo = dateDueTo
|
||||
// ? `${dateDueTo.getDate()}.${dateDueTo.getMonth()}.${dateDueTo.getFullYear()}`
|
||||
// : "-";
|
||||
|
||||
// if (item.privileges.length) {
|
||||
// const result = item.privileges.reduce((acc, privilege) => {
|
||||
// acc = acc
|
||||
// ? `${acc}, ${privilege.good} - ${privilege.discount}%`
|
||||
// : `${privilege.good} - ${Math.round(privilege.discount * 100)}%`;
|
||||
|
||||
// return acc;
|
||||
// }, "");
|
||||
|
||||
// return {
|
||||
// ...item, privileges: result, from: strFrom, dueTo: strDueTo,
|
||||
// basketMore: basketMorePerc, toTime: toTimePerc, toCapacity: toCapacityPerc
|
||||
// };
|
||||
// } else {
|
||||
// return {
|
||||
// ...item, from: strFrom, dueTo: strDueTo,
|
||||
// basketMore: basketMorePerc, toTime: toTimePerc, toCapacity: toCapacityPerc
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
||||
const discountsGridData: GridRowsProp = discounts.map(discount => {
|
||||
return { // TODO
|
||||
id: discount._id,
|
||||
name: discount.name,
|
||||
description: discount.description,
|
||||
conditionType: discount.conditionType,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(discountsGridData);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
width: "90%",
|
||||
height: "60px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
color: theme.palette.secondary.main
|
||||
}}>
|
||||
СКИДКИ
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "left",
|
||||
alignItems: "left",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Название"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{
|
||||
height: "30px",
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={discountNameField}
|
||||
onChange={e => setDiscountNameField(e.target.value)}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
width: "90%",
|
||||
height: "40px",
|
||||
fontWeight: "normal",
|
||||
color: theme.palette.grayDisabled.main,
|
||||
marginTop: "75px",
|
||||
paddingLeft: '10px',
|
||||
}}>
|
||||
Условия:
|
||||
</Typography>
|
||||
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={serviceType}
|
||||
label="Age"
|
||||
onChange={handleServiceTypeChange}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.secondary.main
|
||||
},
|
||||
".MuiSvgIcon-root ": {
|
||||
fill: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value={"Шаблонизатор"}>Шаблонизатор</MenuItem>
|
||||
<MenuItem value={"Опросник"}>Опросник</MenuItem>
|
||||
<MenuItem value={"Аналитика сокращателя"}>Аналитика сокращателя</MenuItem>
|
||||
<MenuItem value={"АБ тесты"}>АБ тесты</MenuItem>
|
||||
</Select>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Процент скидки"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={discountFactor}
|
||||
onChange={e => setDiscountFactor(Number(e.target.value) || 1)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Внесено больше"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={purchasesAmountThreshold}
|
||||
onChange={e => setPurchasesAmountThreshold(Number(e.target.value) || 0)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Объем в корзине"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={cartPurchasesAmountThreshold}
|
||||
onChange={e => setCartPurchasesAmountThreshold(Number(e.target.value) || 0)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Минимальное значение"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={discountMinValue}
|
||||
onChange={e => setDiscountMinValue(Number(e.target.value) || 0)}
|
||||
/>
|
||||
|
||||
<TableContainer component={Paper} sx={{
|
||||
width: "100%",
|
||||
marginTop: "35px",
|
||||
backgroundColor: theme.palette.content.main
|
||||
}}>
|
||||
<Table aria-label="simple table">
|
||||
<TableBody>
|
||||
<TableRow sx={{ border: "1px solid white" }} >
|
||||
<TableCell component="th" scope="row" sx={{ color: theme.palette.secondary.main }}>
|
||||
Работает, если заплатите 100500 денег
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow sx={{ border: "1px solid white" }} >
|
||||
<TableCell component="th" scope="row" sx={{ color: theme.palette.secondary.main }}>
|
||||
Вы должны будете продать душу дьяволу
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
width: "90%",
|
||||
height: "40px",
|
||||
fontWeight: "normal",
|
||||
color: theme.palette.grayDisabled.main,
|
||||
marginTop: "55px"
|
||||
}}>
|
||||
Дата действия:
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
<Typography sx={{
|
||||
width: "35px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "left",
|
||||
}}>С</Typography>
|
||||
|
||||
<DesktopDatePicker
|
||||
inputFormat="DD/MM/YYYY"
|
||||
value={startDate}
|
||||
onChange={(e) => { if (e) { setStartDate(e); } }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
InputProps={{
|
||||
sx: {
|
||||
height: "40px",
|
||||
color: theme.palette.secondary.main,
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography sx={{
|
||||
width: "65px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>по</Typography>
|
||||
|
||||
<DesktopDatePicker
|
||||
inputFormat="DD/MM/YYYY"
|
||||
value={endDate}
|
||||
onChange={(e) => { if (e) { setEndDate(e); } }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
InputProps={{
|
||||
sx: {
|
||||
height: "40px",
|
||||
color: theme.palette.secondary.main,
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
width: "90%",
|
||||
marginTop: theme.spacing(2),
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: "20px",
|
||||
height: "42px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "left",
|
||||
alignItems: "left",
|
||||
marginRight: theme.spacing(1)
|
||||
}}>
|
||||
<Checkbox
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
"&.Mui-checked": {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
}}
|
||||
checked={isInfinite}
|
||||
onClick={() => setIsInfinite(p => !p)}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
Бессрочно
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
width: "90%",
|
||||
marginTop: "55px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.menu.main,
|
||||
height: "52px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}
|
||||
onClick={createDiscount}
|
||||
>Cоздать</Button>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
<Box style={{ width: "80%", marginTop: "55px" }}>
|
||||
<Box style={{ height: 400 }}>
|
||||
<DataGrid
|
||||
checkboxSelection={true}
|
||||
rows={discountsGridData}
|
||||
columns={columns}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
"& .MuiDataGrid-iconSeparator": {
|
||||
display: "none"
|
||||
},
|
||||
"& .css-levciy-MuiTablePagination-displayedRows": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiTablePagination-selectLabel": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiButton-text": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
}}
|
||||
components={{ Toolbar: GridToolbar }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: '100%',
|
||||
marginTop: "45px"
|
||||
}}>
|
||||
<BoxButton sx={{
|
||||
maxWidth: "420px",
|
||||
width: '100%',
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={activateDiscounts}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.menu.main,
|
||||
width: "200px",
|
||||
height: "48px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
marginBottom: '10px',
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}>
|
||||
Деактивировать
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={deactivateDiscounts}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.menu.main,
|
||||
width: "200px",
|
||||
height: "48px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}>
|
||||
Применить
|
||||
</Button>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
|
||||
</LocalizationProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default DiscountManagement;
|
@ -1,697 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Box, Typography, TextField, Checkbox, Button } from "@mui/material";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { DataGrid, GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
import theme from "../../../../theme";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { Discount } from "../../../../model/cart";
|
||||
import { useDiscountStore } from "../../../../stores/discounts";
|
||||
|
||||
|
||||
const BoxButton = styled('div')(({ theme }) => ({
|
||||
[theme.breakpoints.down(400)]: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "id",
|
||||
headerName: "ID",
|
||||
width: 30,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название скидки",
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "endless",
|
||||
headerName: "Бесконечная",
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "from",
|
||||
headerName: "От",
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "dueTo",
|
||||
headerName: "До",
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "privileges",
|
||||
headerName: "Привилегии",
|
||||
width: 210,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "active",
|
||||
headerName: "Активна",
|
||||
width: 100,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "basketMore",
|
||||
headerName: "Корзина больше",
|
||||
width: 140,
|
||||
sortable: false,
|
||||
}
|
||||
,
|
||||
{
|
||||
field: "toTime",
|
||||
headerName: "На время",
|
||||
width: 140,
|
||||
sortable: false,
|
||||
}
|
||||
,
|
||||
{
|
||||
field: "toCapacity",
|
||||
headerName: "На объем",
|
||||
width: 140,
|
||||
sortable: false,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const Discounts: React.FC = () => {
|
||||
const [checkboxState, setCheckboxState] = React.useState<boolean>(false);
|
||||
const toggleCheckbox = () => { setCheckboxState(!checkboxState); };
|
||||
|
||||
const [value1, setValue1] = React.useState<Date>(new Date());
|
||||
const [value2, setValue2] = React.useState<Date>(new Date());
|
||||
|
||||
const [service, setService] = React.useState("Шаблонизатор");
|
||||
const handleChange = (event: SelectChangeEvent) => {
|
||||
setService(event.target.value as string);
|
||||
};
|
||||
|
||||
const discountsArray = useDiscountStore(state => state.discountsArray);
|
||||
const discountsArraySet = useDiscountStore(state => state.setDiscountsArray);
|
||||
|
||||
const discountsActiveArray = useDiscountStore(state => state.discountsActiveArray);
|
||||
const discountsActiveArraySet = useDiscountStore(state => state.setDiscountsActiveArray);
|
||||
let discountsActiveArrayUpdated: Array<number>;
|
||||
|
||||
const findActiveDiscounts = () => {
|
||||
const actives: Array<number> = [];
|
||||
|
||||
discountsArray.forEach((item, i) => {
|
||||
if (item.active == true) { actives.push(i); }
|
||||
});
|
||||
|
||||
discountsActiveArrayUpdated = [...actives];
|
||||
|
||||
if (JSON.stringify(discountsActiveArray) != JSON.stringify(discountsActiveArrayUpdated)) {
|
||||
discountsActiveArraySet(discountsActiveArrayUpdated);
|
||||
}
|
||||
};
|
||||
|
||||
findActiveDiscounts();
|
||||
|
||||
const discountsArrayConverted = discountsArray.map((item) => {
|
||||
const basketMorePerc = Math.round(Number(item.basketMore) * 100) + "%";
|
||||
const toTimePerc = Math.round(Number(item.toTime) * 100) + "%";
|
||||
const toCapacityPerc = Math.round(Number(item.toCapacity) * 100) + "%";
|
||||
|
||||
const dateFrom = item.from ? new Date(Number(item.from)) : "";
|
||||
const dateDueTo = item.from ? new Date(Number(item.dueTo)) : "";
|
||||
|
||||
const strFrom = dateFrom
|
||||
? `${dateFrom.getDate()}.${dateFrom.getMonth()}.${dateFrom.getFullYear()}`
|
||||
: "-";
|
||||
|
||||
const strDueTo = dateDueTo
|
||||
? `${dateDueTo.getDate()}.${dateDueTo.getMonth()}.${dateDueTo.getFullYear()}`
|
||||
: "-";
|
||||
|
||||
if (item.privileges.length) {
|
||||
const result = item.privileges.reduce((acc, privilege) => {
|
||||
acc = acc
|
||||
? `${acc}, ${privilege.good} - ${privilege.discount}%`
|
||||
: `${privilege.good} - ${Math.round(privilege.discount * 100)}%`;
|
||||
|
||||
return acc;
|
||||
}, "");
|
||||
|
||||
return {
|
||||
...item, privileges: result, from: strFrom, dueTo: strDueTo,
|
||||
basketMore: basketMorePerc, toTime: toTimePerc, toCapacity: toCapacityPerc
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...item, from: strFrom, dueTo: strDueTo,
|
||||
basketMore: basketMorePerc, toTime: toTimePerc, toCapacity: toCapacityPerc
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const createDiscount = (name: string,
|
||||
discount: number,
|
||||
addedMore: number,
|
||||
basketMore: number,
|
||||
toTime: number,
|
||||
toCapacity: number,) => {
|
||||
const newDiscount = {
|
||||
id: new Date().getTime(),
|
||||
name,
|
||||
endless: checkboxState,
|
||||
incomeMore: addedMore,
|
||||
from: checkboxState ? "" : new Date(value1).getTime() + "",
|
||||
dueTo: checkboxState ? "" : new Date(value2).getTime() + "",
|
||||
privileges: [{
|
||||
good: service,
|
||||
discount: discount / 100
|
||||
}],
|
||||
active: false,
|
||||
basketMore: basketMore / 100,
|
||||
toTime: toTime / 100,
|
||||
toCapacity: toCapacity / 100
|
||||
} as Discount;
|
||||
|
||||
const discountsArrayUpdated = [...discountsArray, newDiscount];
|
||||
discountsArraySet(discountsArrayUpdated);
|
||||
};
|
||||
|
||||
const fieldName = React.useRef<HTMLInputElement | null>(null);
|
||||
const fieldDiscount = React.useRef<HTMLInputElement | null>(null);
|
||||
const fieldAddedMore = React.useRef<HTMLInputElement | null>(null);
|
||||
const basketMore = React.useRef<HTMLInputElement | null>(null);
|
||||
const toTime = React.useRef<HTMLInputElement | null>(null);
|
||||
const toCapacity = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// const cleraAddedMore = () => {
|
||||
// if (fieldAddedMore.current) {
|
||||
// fieldAddedMore.current.value = "";
|
||||
// }
|
||||
// }
|
||||
|
||||
const checkFields = () => {
|
||||
if (fieldName.current != null
|
||||
&& fieldDiscount.current != null
|
||||
&& fieldAddedMore.current != null
|
||||
&& basketMore.current != null
|
||||
&& toTime.current != null
|
||||
&& toCapacity.current != null) {
|
||||
|
||||
createDiscount(fieldName.current.value,
|
||||
Number(fieldDiscount.current.value),
|
||||
Number(fieldAddedMore.current.value),
|
||||
Number(basketMore.current.value),
|
||||
Number(toTime.current.value),
|
||||
Number(toCapacity.current.value));
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const discountsSelectedRowsData = useDiscountStore(state => state.discountsSelectedRowsData);
|
||||
const discountsSelectedRowsDataSet = useDiscountStore(state => state.setDiscountsSelectedRowsData);
|
||||
|
||||
const onRowsSelectionHandler = (ids: GridSelectionModel) => {
|
||||
const result: Array<Discount> = [];
|
||||
ids.forEach((id) => discountsArray.forEach((row) => {
|
||||
if (row.id === id) result.push(row);
|
||||
}));
|
||||
|
||||
discountsSelectedRowsDataSet([...result]);
|
||||
};
|
||||
|
||||
const activation = (value: boolean) => {
|
||||
discountsArray.forEach((item, i) => {
|
||||
discountsSelectedRowsData.forEach((selected) => {
|
||||
if (item.id == selected.id) {
|
||||
if (value) {
|
||||
discountsArray[i].active = true;
|
||||
} else {
|
||||
discountsArray[i].active = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
discountsArraySet(discountsArray);
|
||||
};
|
||||
|
||||
const PositiveInput = (event: any) => {
|
||||
const numberInput = parseInt(event.target.value);
|
||||
if (isNaN(numberInput) || numberInput < 0) { event.target.value = '0'; }
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
width: "90%",
|
||||
height: "60px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
color: theme.palette.secondary.main
|
||||
}}>
|
||||
СКИДКИ
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "left",
|
||||
alignItems: "left",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Название"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{
|
||||
height: "30px",
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={fieldName}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
width: "90%",
|
||||
height: "40px",
|
||||
fontWeight: "normal",
|
||||
color: theme.palette.grayDisabled.main,
|
||||
marginTop: "75px",
|
||||
paddingLeft: '10px',
|
||||
}}>
|
||||
Условия:
|
||||
</Typography>
|
||||
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={service}
|
||||
label="Age"
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.secondary.main
|
||||
},
|
||||
".MuiSvgIcon-root ": {
|
||||
fill: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value={"Шаблонизатор"}>Шаблонизатор</MenuItem>
|
||||
<MenuItem value={"Опросник"}>Опросник</MenuItem>
|
||||
<MenuItem value={"Аналитика сокращателя"}>Аналитика сокращателя</MenuItem>
|
||||
<MenuItem value={"АБ тесты"}>АБ тесты</MenuItem>
|
||||
</Select>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Процент скидки"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={fieldDiscount}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Внесено больше"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={fieldAddedMore}
|
||||
onChange={PositiveInput}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Объем в корзине"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={basketMore}
|
||||
onChange={PositiveInput}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"На время"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={toTime}
|
||||
onChange={PositiveInput}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"На объем"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
marginTop: "15px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={toCapacity}
|
||||
onChange={PositiveInput}
|
||||
/>
|
||||
|
||||
<TableContainer component={Paper} sx={{
|
||||
width: "100%",
|
||||
marginTop: "35px",
|
||||
backgroundColor: theme.palette.content.main
|
||||
}}>
|
||||
<Table aria-label="simple table">
|
||||
<TableBody>
|
||||
<TableRow sx={{ border: "1px solid white" }} >
|
||||
<TableCell component="th" scope="row" sx={{ color: theme.palette.secondary.main }}>
|
||||
Работает, если заплатите 100500 денег
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow sx={{ border: "1px solid white" }} >
|
||||
<TableCell component="th" scope="row" sx={{ color: theme.palette.secondary.main }}>
|
||||
Вы должны будете продать душу дьяволу
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
width: "90%",
|
||||
height: "40px",
|
||||
fontWeight: "normal",
|
||||
color: theme.palette.grayDisabled.main,
|
||||
marginTop: "55px"
|
||||
}}>
|
||||
Дата действия:
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
<Typography sx={{
|
||||
width: "35px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "left",
|
||||
}}>С</Typography>
|
||||
|
||||
<DesktopDatePicker
|
||||
inputFormat="DD/MM/YYYY"
|
||||
value={value1}
|
||||
onChange={(e) => { if (e) { setValue1(e); } }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
InputProps={{
|
||||
sx: {
|
||||
height: "40px",
|
||||
color: theme.palette.secondary.main,
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography sx={{
|
||||
width: "65px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>по</Typography>
|
||||
|
||||
<DesktopDatePicker
|
||||
inputFormat="DD/MM/YYYY"
|
||||
value={value2}
|
||||
onChange={(e) => { if (e) { setValue2(e); } }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
InputProps={{
|
||||
sx: {
|
||||
height: "40px",
|
||||
color: theme.palette.secondary.main,
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
width: "90%",
|
||||
marginTop: theme.spacing(2),
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: "20px",
|
||||
height: "42px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "left",
|
||||
alignItems: "left",
|
||||
marginRight: theme.spacing(1)
|
||||
}}>
|
||||
<Checkbox sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
"&.Mui-checked": {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
}} onClick={() => toggleCheckbox()} />
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
Бессрочно
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
width: "90%",
|
||||
marginTop: "55px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.menu.main,
|
||||
height: "52px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}
|
||||
onClick={() => checkFields()} >
|
||||
Cоздать
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
<Box style={{ width: "80%", marginTop: "55px" }}>
|
||||
<Box style={{ height: 400 }}>
|
||||
<DataGrid
|
||||
checkboxSelection={true}
|
||||
rows={discountsArrayConverted}
|
||||
columns={columns}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
"& .MuiDataGrid-iconSeparator": {
|
||||
display: "none"
|
||||
},
|
||||
"& .css-levciy-MuiTablePagination-displayedRows": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiTablePagination-selectLabel": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiButton-text": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
}}
|
||||
components={{ Toolbar: GridToolbar }}
|
||||
onSelectionModelChange={(ids) => onRowsSelectionHandler(ids)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: '100%',
|
||||
marginTop: "45px"
|
||||
}}>
|
||||
<BoxButton sx={{
|
||||
maxWidth: "420px",
|
||||
width: '100%',
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => activation(false)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.menu.main,
|
||||
width: "200px",
|
||||
height: "48px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
marginBottom: '10px',
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}>
|
||||
Деактивировать
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => activation(true)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.menu.main,
|
||||
width: "200px",
|
||||
height: "48px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}>
|
||||
Применить
|
||||
</Button>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
|
||||
</LocalizationProvider>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Discounts;
|
@ -6,7 +6,7 @@ import TableHead from '@mui/material/TableHead';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import theme from "../../../../theme";
|
||||
import theme from "../../../theme";
|
||||
|
||||
|
||||
const Users: React.FC = () => {
|
@ -1,4 +1,3 @@
|
||||
import * as React from "react";
|
||||
import { Box, Typography, TextField, Checkbox, Button } from "@mui/material";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
@ -12,10 +11,10 @@ import Paper from "@mui/material/Paper";
|
||||
import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
import theme from "../../../../theme";
|
||||
import { Promocode } from "../../../../model/cart";
|
||||
import { ServiceType } from "../../../../model/tariff";
|
||||
import { usePromocodeStore } from "../../../../stores/promocodes";
|
||||
import theme from "../../../theme";
|
||||
import { usePromocodeStore } from "../../../stores/promocodes";
|
||||
import { useRef, useState } from "react";
|
||||
import { ServiceType } from "@root/model/tariff";
|
||||
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
@ -57,76 +56,84 @@ const columns: GridColDef[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const Promocodes: React.FC = () => {
|
||||
const [checkboxState, setCheckboxState] = React.useState<boolean>(false);
|
||||
const PromocodeManagement: React.FC = () => {
|
||||
const [checkboxState, setCheckboxState] = useState<boolean>(false);
|
||||
const toggleCheckbox = () => { setCheckboxState(!checkboxState); };
|
||||
|
||||
const [value1, setValue1] = React.useState<Date>(new Date());
|
||||
const [value2, setValue2] = React.useState<Date>(new Date());
|
||||
const [value1, setValue1] = useState<Date>(new Date());
|
||||
const [value2, setValue2] = useState<Date>(new Date());
|
||||
|
||||
const [service, setService] = React.useState<ServiceType>("Шаблонизатор документов");
|
||||
const [service, setService] = useState<ServiceType>("templategen");
|
||||
const handleChange = (event: SelectChangeEvent) => {
|
||||
setService(event.target.value as ServiceType);
|
||||
};
|
||||
|
||||
const promocodeArray = usePromocodeStore(state => state.promocodeArray);
|
||||
const promocodeArraySet = usePromocodeStore(state => state.setPromocodeArray);
|
||||
const promocodes = usePromocodeStore(state => state.promocodes);
|
||||
const addPromocodes = usePromocodeStore(state => state.addPromocodes);
|
||||
|
||||
const promocodeArrayConverted = promocodeArray.map((item) => {
|
||||
const dateFrom = item.from ? new Date(Number(item.from)) : "";
|
||||
const dateDueTo = item.from ? new Date(Number(item.dueTo)) : "";
|
||||
|
||||
const strFrom = dateFrom
|
||||
? `${dateFrom.getDate()}.${dateFrom.getMonth()}.${dateFrom.getFullYear()}`
|
||||
: "-";
|
||||
|
||||
const strDueTo = dateDueTo
|
||||
? `${dateDueTo.getDate()}.${dateDueTo.getMonth()}.${dateDueTo.getFullYear()}`
|
||||
: "-";
|
||||
|
||||
if (item.privileges.length) {
|
||||
const result = item.privileges.reduce((acc, privilege) => {
|
||||
acc = acc
|
||||
? `${acc}, ${privilege.good} - ${privilege.discount}%`
|
||||
: `${privilege.good} - ${privilege.discount * 100}%`;
|
||||
|
||||
return acc;
|
||||
}, "");
|
||||
|
||||
return { ...item, privileges: result, from: strFrom, dueTo: strDueTo };
|
||||
} else {
|
||||
return { ...item, from: strFrom, dueTo: strDueTo };
|
||||
function createPromocode() {
|
||||
// TODO
|
||||
}
|
||||
});
|
||||
|
||||
const createPromocode = (name: string, discount: number) => {
|
||||
const newPromocode = {
|
||||
id: new Date().getTime(),
|
||||
name,
|
||||
endless: checkboxState,
|
||||
from: checkboxState ? "" : new Date(value1).getTime() + "",
|
||||
dueTo: checkboxState ? "" : new Date(value2).getTime() + "",
|
||||
privileges: [{
|
||||
good: service,
|
||||
discount: discount / 100
|
||||
}]
|
||||
};
|
||||
// const promocodeArrayConverted = promocodes.map((item) => {
|
||||
// const dateFrom = item.from ? new Date(Number(item.from)) : "";
|
||||
// const dateDueTo = item.from ? new Date(Number(item.dueTo)) : "";
|
||||
|
||||
const promocodeArrayUpdated = [...promocodeArray, newPromocode];
|
||||
promocodeArraySet(promocodeArrayUpdated);
|
||||
};
|
||||
// const strFrom = dateFrom
|
||||
// ? `${dateFrom.getDate()}.${dateFrom.getMonth()}.${dateFrom.getFullYear()}`
|
||||
// : "-";
|
||||
|
||||
const fieldName = React.useRef<HTMLInputElement | null>(null);
|
||||
const fieldDiscount = React.useRef<HTMLInputElement | null>(null);
|
||||
// const strDueTo = dateDueTo
|
||||
// ? `${dateDueTo.getDate()}.${dateDueTo.getMonth()}.${dateDueTo.getFullYear()}`
|
||||
// : "-";
|
||||
|
||||
const checkFields = () => {
|
||||
if (fieldName.current != null && fieldDiscount.current != null) {
|
||||
createPromocode(fieldName.current.value, Number(fieldDiscount.current.value));
|
||||
}
|
||||
};
|
||||
// if (item.privileges.length) {
|
||||
// const result = item.privileges.reduce((acc, privilege) => {
|
||||
// acc = acc
|
||||
// ? `${acc}, ${privilege.serviceKey} - ${privilege.discount}%`
|
||||
// : `${privilege.serviceKey} - ${privilege.discount * 100}%`;
|
||||
|
||||
// return acc;
|
||||
// }, "");
|
||||
|
||||
// return { ...item, privileges: result, from: strFrom, dueTo: strDueTo };
|
||||
// } else {
|
||||
// return { ...item, from: strFrom, dueTo: strDueTo };
|
||||
// }
|
||||
// });
|
||||
|
||||
// const createPromocode = (name: string, discount: number) => {
|
||||
// const newPromocode = {
|
||||
// id: new Date().getTime(),
|
||||
// name,
|
||||
// endless: checkboxState,
|
||||
// from: checkboxState ? "" : new Date(value1).getTime() + "",
|
||||
// dueTo: checkboxState ? "" : new Date(value2).getTime() + "",
|
||||
// privileges: [{
|
||||
// good: service,
|
||||
// discount: discount / 100
|
||||
// }]
|
||||
// };
|
||||
|
||||
// const promocodeArrayUpdated = [...promocodes, newPromocode];
|
||||
// addPromocodes(promocodeArrayUpdated);
|
||||
// };
|
||||
|
||||
const promocodeGridData = promocodes.map(procomode => {
|
||||
// TODO
|
||||
})
|
||||
|
||||
const fieldName = useRef<HTMLInputElement | null>(null);
|
||||
const fieldDiscount = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// const checkFields = () => {
|
||||
// if (fieldName.current != null && fieldDiscount.current != null) {
|
||||
// createPromocode(fieldName.current.value, Number(fieldDiscount.current.value));
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
@ -380,7 +387,7 @@ const Promocodes: React.FC = () => {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}
|
||||
onClick={() => checkFields()} >
|
||||
onClick={createPromocode} >
|
||||
Cоздать
|
||||
</Button>
|
||||
</Box>
|
||||
@ -391,7 +398,7 @@ const Promocodes: React.FC = () => {
|
||||
<Box style={{ height: 400 }}>
|
||||
<DataGrid
|
||||
checkboxSelection={true}
|
||||
rows={promocodeArrayConverted}
|
||||
rows={promocodeGridData}
|
||||
columns={columns}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
@ -421,9 +428,9 @@ const Promocodes: React.FC = () => {
|
||||
</Box>
|
||||
|
||||
</LocalizationProvider>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Promocodes;
|
||||
export default PromocodeManagement;
|
@ -1,235 +0,0 @@
|
||||
import { Avatar, Box, Checkbox, FormControlLabel, IconButton, List, ListItem, ListItemAvatar, ListItemText, TextField, Typography, useTheme } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { useMemo, useState } from "react";
|
||||
import { calcFitDiscounts, calcTotalAndRowData, separator } from "./utils";
|
||||
import type { CartSummary, Promocode } from "../../../../model/cart";
|
||||
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
|
||||
import { useDiscountStore } from "../../../../stores/discounts";
|
||||
import { useCartStore } from "../../../../stores/cart";
|
||||
|
||||
|
||||
interface Props {
|
||||
promocode?: Promocode;
|
||||
}
|
||||
|
||||
export default function Cart({ promocode }: Props) {
|
||||
const theme = useTheme();
|
||||
const discountsArray = useDiscountStore(state => state.discountsArray);
|
||||
const discountsActiveArray = useDiscountStore(state => state.discountsActiveArray);
|
||||
const cartRowsData = useCartStore(state => state.cartRowsData);
|
||||
const cartRowsDataSet = useCartStore(state => state.setCartRowsData);
|
||||
const [isNonCommercial, setIsNonCommercial] = useState(false);
|
||||
const [addedValueField, setAddedValueField] = useState("");
|
||||
|
||||
const cartSummary = useMemo(() => {
|
||||
const cartSum: { [key: string]: CartSummary; } = {};
|
||||
cartRowsData.forEach((row) => {
|
||||
const prev = cartSum[row.service];
|
||||
cartSum[row.service] = {
|
||||
mbs: row.disk + (prev?.mbs || 0),
|
||||
points: row.points + (prev?.points || 0),
|
||||
days: row.time + (prev?.days || 0),
|
||||
};
|
||||
});
|
||||
return cartSum;
|
||||
}, [cartRowsData]);
|
||||
|
||||
const fitDiscounts = useMemo(() =>
|
||||
calcFitDiscounts(discountsArray, discountsActiveArray, cartSummary, addedValueField),
|
||||
[addedValueField, cartSummary, discountsActiveArray, discountsArray]
|
||||
);
|
||||
|
||||
const { totalPrice, calculatedCartRowData } = useMemo(() => calcTotalAndRowData(
|
||||
cartRowsData,
|
||||
isNonCommercial,
|
||||
discountsArray,
|
||||
discountsActiveArray,
|
||||
fitDiscounts,
|
||||
addedValueField,
|
||||
cartSummary,
|
||||
promocode,
|
||||
), [addedValueField, cartRowsData, cartSummary, discountsActiveArray, discountsArray, fitDiscounts, isNonCommercial, promocode]);
|
||||
|
||||
const { resultPrice, discountText } = useMemo(() => {
|
||||
const discounts: Array<number> = [];
|
||||
|
||||
let resultPrice = totalPrice;
|
||||
|
||||
if (isNonCommercial) {
|
||||
resultPrice *= 0.2;
|
||||
return { resultPrice, discountText: `80%` };
|
||||
}
|
||||
|
||||
// применяем активные скидки за объем корзины
|
||||
if (fitDiscounts.length >= 0 && !isNonCommercial && !promocode) {
|
||||
fitDiscounts.forEach(activeDiscount => {
|
||||
const discount = discountsArray[activeDiscount];
|
||||
if ((discount.basketMore > 0 && totalPrice > discount.basketMore && discount.basketMore === fitDiscounts.reduce((a, e) => Math.max(a, discountsArray[e].basketMore), 0)) ||
|
||||
(discount.incomeMore > 0 && parseInt(addedValueField) > discount.incomeMore)) {
|
||||
resultPrice *= (1 - discount.privileges[0].discount);
|
||||
discounts.push(discount.privileges[0].discount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const discountText = discounts.map(e => `${(e * 100).toFixed(2)}%`).join(' × ') + ` = ${(100 - discounts.reduce((a: number, cv: number) => a * (1 - cv), 100)).toFixed(2)}%`;
|
||||
|
||||
return {
|
||||
resultPrice,
|
||||
discountText,
|
||||
};
|
||||
}, [addedValueField, discountsArray, fitDiscounts, isNonCommercial, promocode, totalPrice]);
|
||||
|
||||
const handleRemoveBasket = (id: number) => cartRowsDataSet(cartRowsData.filter((row) => row.id !== id));
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: "45px"
|
||||
}}>
|
||||
<Typography id="transition-modal-title" variant="caption">
|
||||
Корзина
|
||||
</Typography>
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
marginTop: "15px",
|
||||
marginBottom: "15px",
|
||||
maxWidth: "350px",
|
||||
width: '100%',
|
||||
justifyContent: "space-between"
|
||||
}}>
|
||||
<FormControlLabel
|
||||
label="НКО"
|
||||
control={<Checkbox
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
"&.Mui-checked": {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
}}
|
||||
onClick={() => setIsNonCommercial(prev => !prev)}
|
||||
/>}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Внесено"}
|
||||
variant="filled"
|
||||
size="small"
|
||||
color="secondary"
|
||||
type="number"
|
||||
sx={{
|
||||
width: "200px"
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={addedValueField}
|
||||
onChange={e => setAddedValueField(Number(e.target.value) >= 0 ? e.target.value : "")}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<List sx={{
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.secondary.main,
|
||||
maxWidth: '745px',
|
||||
width: '100%',
|
||||
}}>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Название"
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
// minWidth: "250px",
|
||||
maxWidth: "250px",
|
||||
padding: '0 10px'
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Цена"
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
// minWidth: "200px",
|
||||
padding: '0 10px',
|
||||
maxWidth: "200px"
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Скидки"
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
// minWidth: "200px",
|
||||
padding: '0 10px',
|
||||
maxWidth: "400px"
|
||||
}}
|
||||
/>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon sx={{
|
||||
color: theme.palette.grayDisabled.main,
|
||||
display: "none"
|
||||
}} />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
|
||||
{calculatedCartRowData.map((cartRow) => (
|
||||
<ListItem key={cartRow.id}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ backgroundColor: theme.palette.secondary.main }}>
|
||||
<ShoppingCartIcon sx={{
|
||||
color: theme.palette.content.main,
|
||||
}} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={cartRow.name}
|
||||
sx={{ maxWidth: "250px" }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={`${separator(cartRow.price)} ₽`}
|
||||
sx={{ textAlign: "center", maxWidth: "200px" }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={`${(cartRow.appliedDiscounts.map(e => (e * 100).toFixed(2)).join(' × '))} = ${(100 - cartRow.appliedDiscounts.reduce((a: number, cv: number) => a * (1 - cv), 100)).toFixed(2)}%`}
|
||||
sx={{ textAlign: "center", maxWidth: "400px" }}
|
||||
/>
|
||||
<IconButton edge="end" aria-label="delete" onClick={() => handleRemoveBasket(cartRow.id)}>
|
||||
<DeleteIcon sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
}} />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
<Typography id="transition-modal-title" variant="h6" sx={{
|
||||
fontWeight: "normal",
|
||||
textAlign: "center",
|
||||
marginTop: "15px",
|
||||
fontSize: "16px"
|
||||
}}>
|
||||
Скидки:   {discountText}
|
||||
</Typography>
|
||||
|
||||
<Typography id="transition-modal-title" variant="h6" sx={{
|
||||
fontWeight: "normal",
|
||||
textAlign: "center",
|
||||
marginTop: "10px"
|
||||
}}>
|
||||
ИТОГО:   {resultPrice} ₽
|
||||
</Typography>
|
||||
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
}
|
165
src/pages/dashboard/Content/Tariffs/CreateTariff.tsx
Normal file
165
src/pages/dashboard/Content/Tariffs/CreateTariff.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { Typography, Container, Button, Select, MenuItem, FormControl, InputLabel, TextField, useTheme, Box } from "@mui/material";
|
||||
import { Tariff } from "@root/model/tariff";
|
||||
import { usePrivilegeStore } from "@root/stores/privileges";
|
||||
import { useTariffStore } from "@root/stores/tariffs";
|
||||
import { nanoid } from "nanoid";
|
||||
import { ChangeEvent, HTMLInputTypeAttribute, useState } from "react";
|
||||
|
||||
|
||||
export default function CreateTariff() {
|
||||
const theme = useTheme();
|
||||
const privileges = usePrivilegeStore(store => store.privileges);
|
||||
const addTariffs = useTariffStore(store => store.addTariffs);
|
||||
const [nameField, setNameField] = useState<string>("");
|
||||
const [amountField, setAmountField] = useState<string>("");
|
||||
const [customPriceField, setCustomPriceField] = useState<string>("");
|
||||
const [privilegeIdField, setPrivilegeIdField] = useState<string | "">("");
|
||||
|
||||
const privilege = privileges.find(p => p.privilegeId === privilegeIdField);
|
||||
|
||||
function handleCreateTariffClick() {
|
||||
const amount = Number(amountField);
|
||||
const customPricePerUnit = Number(customPriceField);
|
||||
|
||||
if (isNaN(amount) || !privilege) return;
|
||||
|
||||
const newTariff: Tariff = {
|
||||
id: nanoid(5),
|
||||
name: nameField,
|
||||
amount,
|
||||
privilege,
|
||||
customPricePerUnit: customPricePerUnit ? customPricePerUnit : undefined,
|
||||
};
|
||||
|
||||
addTariffs([newTariff]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container sx={{
|
||||
p: "20px",
|
||||
border: "1px solid rgba(224, 224, 224, 1)",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
}}>
|
||||
<Typography variant="h6" sx={{ textAlign: "center", mb: "16px" }}>Создание тарифа</Typography>
|
||||
<FormControl
|
||||
fullWidth
|
||||
sx={{
|
||||
height: "52px",
|
||||
color: theme.palette.secondary.main,
|
||||
"& .MuiInputLabel-outlined": {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
"& .MuiInputLabel-outlined.MuiInputLabel-shrink": {
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputLabel
|
||||
id="privilege-select-label"
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}
|
||||
>Привелегия</InputLabel>
|
||||
<Select
|
||||
labelId="privilege-select-label"
|
||||
id="privilege-select"
|
||||
value={privilegeIdField}
|
||||
label="Привелегия"
|
||||
onChange={e => setPrivilegeIdField(e.target.value)}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
borderColor: theme.palette.secondary.main,
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.secondary.main,
|
||||
border: "1px solid",
|
||||
},
|
||||
".MuiSvgIcon-root ": {
|
||||
fill: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
inputProps={{ sx: { pt: "12px" } }}
|
||||
>
|
||||
{privileges.map((privilege, index) => (
|
||||
<MenuItem key={index} value={privilege.privilegeId}>
|
||||
{privilege.description}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{privilege &&
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<Typography>Имя: <span>{privilege.name}</span></Typography>
|
||||
<Typography>Сервис: <span>{privilege.serviceKey}</span></Typography>
|
||||
<Typography>Единица: <span>{privilege.type}</span></Typography>
|
||||
<Typography>Стандартная цена за единицу: <span>{privilege.pricePerUnit}</span></Typography>
|
||||
</Box>
|
||||
}
|
||||
<CustomTextField
|
||||
id="tariff-name"
|
||||
label="Название тарифа"
|
||||
value={nameField}
|
||||
setValue={e => setNameField(e.target.value)}
|
||||
/>
|
||||
<CustomTextField
|
||||
id="tariff-amount"
|
||||
label="Кол-во единиц привилегии"
|
||||
value={amountField}
|
||||
setValue={e => setAmountField(e.target.value)}
|
||||
type="number"
|
||||
/>
|
||||
<CustomTextField
|
||||
id="tariff-custom-price"
|
||||
label="Кастомная цена за единицу (не обязательно)"
|
||||
value={customPriceField}
|
||||
setValue={e => setCustomPriceField(e.target.value)}
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleCreateTariffClick}
|
||||
disabled={privilegeIdField === "" || amountField === "" || nameField === ""}
|
||||
>Создать</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomTextField({ id, label, value, type, setValue }: {
|
||||
id: string;
|
||||
label: string;
|
||||
value: number | string | null;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
setValue: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
id={id}
|
||||
label={label}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
type={type}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={value ? value : ""}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Box, Typography, TextField } from "@mui/material";
|
||||
import { DataGrid, GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
|
||||
import theme from "../../../../../theme";
|
||||
import { useMemo, useState } from "react";
|
||||
import { convertTariffs, formatPromocodePriveleges } from "../utils";
|
||||
import CustomButton from "../CustomButton";
|
||||
import Cart from "../Cart";
|
||||
import type { ArrayProps } from "../../../../../model/tariff";
|
||||
import { Promocode } from "../../../../../model/cart";
|
||||
import { usePromocodeStore } from "../../../../../stores/promocodes";
|
||||
import { useTariffStore } from "../../../../../stores/tariffs";
|
||||
import { useCartStore } from "../../../../../stores/cart";
|
||||
|
||||
export interface MWProps {
|
||||
openModal: () => void;
|
||||
}
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "id",
|
||||
headerName: "ID",
|
||||
width: 30,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название тарифа",
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "service",
|
||||
headerName: "Сервис",
|
||||
width: 210,
|
||||
sortable: false,
|
||||
}, {
|
||||
field: "disk",
|
||||
headerName: "Гигабайты",
|
||||
type: "number",
|
||||
width: 110,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "time",
|
||||
headerName: "Время",
|
||||
type: "number",
|
||||
width: 110,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "points",
|
||||
headerName: "Объем",
|
||||
width: 110,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "price",
|
||||
headerName: "Стоимость",
|
||||
width: 160,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: "conditions",
|
||||
headerName: "Условия",
|
||||
width: 110,
|
||||
sortable: false,
|
||||
},
|
||||
];
|
||||
|
||||
/* const testTariffArray: ArrayProps[] = [
|
||||
{ id: 1, name: "Тариф 1", type: "tariff", service: "Шаблонизатор документов", disk: 100, time: 200, points: 300, price: 100500 },
|
||||
{ id: 2, name: "Тариф 2", type: "tariff", service: "Шаблонизатор документов", disk: 100, time: 200, points: 300, price: 100500 },
|
||||
{ id: 3, name: "Тариф 3", type: "tariff", service: "Шаблонизатор документов", disk: 100, time: 200, points: 300, price: 100500 },
|
||||
{
|
||||
id: 4, name: "Пакет 1", type: "package", tariffs: [
|
||||
{ id: 1, name: "Тариф 1", type: "tariff", service: "Шаблонизатор документов", disk: 100, time: 200, points: 300, price: 100500 },
|
||||
{ id: 2, name: "Тариф 2", type: "tariff", service: "Шаблонизатор документов", disk: 100, time: 200, points: 300, price: 100500 },
|
||||
]
|
||||
},
|
||||
]; */
|
||||
|
||||
const DataGridElement: React.FC<MWProps> = ({ openModal }) => {
|
||||
const promocodeArray = usePromocodeStore(state => state.promocodeArray);
|
||||
const tariffsSelectedRowsData = useTariffStore(state => state.tariffsSelectedRowsData);
|
||||
const tariffsSelectedRowsDataSet = useTariffStore(state => state.setTariffsSelectedRowsData);
|
||||
const tariffsArray = useTariffStore(state => state.tariffs);
|
||||
const cartRowsDataSet = useCartStore(state => state.setCartRowsData);
|
||||
const [selectedPromocodeIndex, setSelectedPromocodeIndex] = useState(-1);
|
||||
const [promocodeField, setPromocodeField] = useState<string>("");
|
||||
|
||||
const onRowsSelectionHandler = (ids: GridSelectionModel) => {
|
||||
const result: Array<ArrayProps> = [];
|
||||
ids.forEach((id) => tariffsArray.forEach((row) => {
|
||||
if (row.id === id) result.push(row);
|
||||
}));
|
||||
|
||||
tariffsSelectedRowsDataSet(result);
|
||||
};
|
||||
|
||||
const handleToBasket = () => cartRowsDataSet(tariffsSelectedRowsData);
|
||||
|
||||
const checkPromocode = () => {
|
||||
setSelectedPromocodeIndex(promocodeArray.findIndex(promocode => promocode.name === promocodeField));
|
||||
};
|
||||
|
||||
const tariffsArrayConverted = useMemo(() => convertTariffs(tariffsArray), [tariffsArray]);
|
||||
|
||||
const promocode = promocodeArray[selectedPromocodeIndex] as Promocode | undefined;
|
||||
|
||||
return (
|
||||
<Box style={{ width: "93%" }}>
|
||||
<Box style={{ height: 400 }}>
|
||||
<DataGrid
|
||||
checkboxSelection={true}
|
||||
rows={tariffsArrayConverted}
|
||||
columns={columns}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
"& .MuiDataGrid-iconSeparator": {
|
||||
display: "none"
|
||||
},
|
||||
"& .css-levciy-MuiTablePagination-displayedRows": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiTablePagination-selectLabel": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
"& .MuiButton-text": {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
}}
|
||||
components={{ Toolbar: GridToolbar }}
|
||||
onSelectionModelChange={(ids) => onRowsSelectionHandler(ids)}
|
||||
/>
|
||||
|
||||
<Box sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<CustomButton
|
||||
onClick={() => openModal()}
|
||||
sx={{
|
||||
padding: "6px 30px",
|
||||
marginTop: "45px",
|
||||
marginBottom: "15px",
|
||||
maxWidth: '320px',
|
||||
width: '100%',
|
||||
}}>
|
||||
Пакетизировать
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
onClick={() => handleToBasket()}
|
||||
sx={{
|
||||
padding: "6px 69px",
|
||||
marginBottom: "95px",
|
||||
}}>
|
||||
Сложить в корзину
|
||||
</CustomButton>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: "45px",
|
||||
}}>
|
||||
<Box sx={{
|
||||
maxWidth: "480px",
|
||||
width: '100%',
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Ввести промокод"}
|
||||
variant="filled"
|
||||
size="small"
|
||||
color="secondary"
|
||||
sx={{
|
||||
width: "200px",
|
||||
height: "30px",
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
value={promocodeField}
|
||||
onChange={e => setPromocodeField(e.target.value)}
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
onClick={() => checkPromocode()}
|
||||
sx={{
|
||||
width: "200px",
|
||||
height: "48px",
|
||||
}}>
|
||||
Готово
|
||||
</CustomButton>
|
||||
</Box>
|
||||
|
||||
{selectedPromocodeIndex >= 0 &&
|
||||
<Box>
|
||||
<Box sx={{ marginTop: "35px", display: "flex" }}>
|
||||
<Typography sx={{ color: theme.palette.grayDisabled.main, minWidth: "150px" }}>
|
||||
Введен промокод:
|
||||
</Typography>
|
||||
<Typography sx={{ width: "100%", textAlign: "center" }}>
|
||||
{promocodeArray[selectedPromocodeIndex].name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Typography sx={{ color: theme.palette.grayDisabled.main, minWidth: "150px" }}>
|
||||
Привилегии:  
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: "100%", textAlign: "center" }}>
|
||||
<Typography>
|
||||
{formatPromocodePriveleges(promocodeArray[selectedPromocodeIndex])}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
|
||||
<Cart promocode={promocode} />
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataGridElement;
|
@ -1,187 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Box, Modal, Fade, Backdrop, Button, TextField } from "@mui/material";
|
||||
import theme from "../../../../../theme";
|
||||
import { ArrayProps } from "../../../../../model/tariff";
|
||||
import { useTariffStore } from "../../../../../stores/tariffs";
|
||||
|
||||
|
||||
export interface MWProps {
|
||||
open: boolean;
|
||||
type: number;
|
||||
variant: number;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const ModalMini = ({ open, type, variant, close }: MWProps) => {
|
||||
let tariffsArray = useTariffStore(state => state.tariffs);
|
||||
const tariffsArraySet = useTariffStore(state => state.setTariffs);
|
||||
|
||||
const types = ["", "Шаблонизатор документов", "Опросник", "Сокращатель ссылок"];
|
||||
const variants = ["Количество", "Срок (дней)", "Количество (гб)"];
|
||||
|
||||
const fieldName = React.useRef<HTMLInputElement | null>(null);
|
||||
const fieldTime = React.useRef<HTMLInputElement | null>(null);
|
||||
const fieldPrice = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const checkTariff = () => {
|
||||
if (fieldName.current != null && fieldTime.current != null && fieldPrice.current != null) {
|
||||
if (fieldName.current.value && fieldTime.current.value && fieldPrice.current.value) {
|
||||
|
||||
const data = [0, 0, 0];
|
||||
|
||||
if (variant === 0) { data[0] = parseInt(fieldTime.current.value); }
|
||||
if (variant === 1) { data[1] = parseInt(fieldTime.current.value); }
|
||||
if (variant === 2) { data[2] = parseInt(fieldTime.current.value); }
|
||||
|
||||
const tariffsArrayNew = [...tariffsArray, {
|
||||
"id": new Date().getTime(),
|
||||
"name": fieldName.current.value,
|
||||
"type": "tariff",
|
||||
"service": types[type],
|
||||
"disk": data[2],
|
||||
"time": data[1],
|
||||
"points": data[0],
|
||||
"price": +fieldPrice.current.value
|
||||
} as ArrayProps];
|
||||
|
||||
tariffsArraySet(tariffsArrayNew);
|
||||
|
||||
close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
open={open}
|
||||
onClose={() => close()}
|
||||
closeAfterTransition
|
||||
BackdropComponent={Backdrop}
|
||||
BackdropProps={{
|
||||
timeout: 500,
|
||||
}}
|
||||
>
|
||||
<Fade in={open}>
|
||||
<Box sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: "350px",
|
||||
height: "350px",
|
||||
bgcolor: theme.palette.menu.main,
|
||||
boxShadow: 24,
|
||||
color: theme.palette.secondary.main,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={types[type]}
|
||||
disabled={true}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{ width: "80%", marginTop: theme.spacing(1) }}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={"Название тарифа"}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{ width: "80%", marginTop: theme.spacing(1) }}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={fieldName}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label={variants[variant]}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{ width: "80%", marginTop: theme.spacing(1) }}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={fieldTime}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
label="Цена"
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
sx={{ width: "80%", marginTop: theme.spacing(1) }}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
}}
|
||||
inputRef={fieldPrice}
|
||||
/>
|
||||
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => checkTariff()}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.grayDark.main,
|
||||
marginTop: "30px",
|
||||
height: "42px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}>
|
||||
Применить
|
||||
</Button>
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ModalMini;
|
@ -1,92 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Box, Modal, Fade, Backdrop, Button, TextField } from "@mui/material";
|
||||
import theme from "../../../../../theme";
|
||||
|
||||
|
||||
export interface MWProps {
|
||||
open: boolean
|
||||
newPackage: (name: string) => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const ModalPackage = ({open, newPackage, close}: MWProps ) => {
|
||||
const fieldName = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const checkName = () => {
|
||||
if( fieldName.current != null ) {
|
||||
newPackage( fieldName.current.value );
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
open={ open }
|
||||
onClose={ () => close() }
|
||||
closeAfterTransition
|
||||
BackdropComponent={Backdrop}
|
||||
BackdropProps={{
|
||||
timeout: 500,
|
||||
}}
|
||||
>
|
||||
<Fade in={open}>
|
||||
<Box sx={{
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: "350px",
|
||||
height: "170px",
|
||||
bgcolor: theme.palette.menu.main,
|
||||
boxShadow: 24,
|
||||
color: theme.palette.secondary.main,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<TextField
|
||||
id = "standard-basic"
|
||||
label = { "Название пакета" }
|
||||
variant = "filled"
|
||||
color = "secondary"
|
||||
sx = {{ width: "80%", marginTop: theme.spacing(1) }}
|
||||
InputProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.content.main,
|
||||
color: theme.palette.secondary.main,
|
||||
} }}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.secondary.main
|
||||
} }}
|
||||
inputRef={ fieldName }
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant = "contained"
|
||||
onClick={ () => checkName() }
|
||||
sx={{
|
||||
backgroundColor: theme.palette.grayDark.main,
|
||||
marginTop: "30px",
|
||||
height: "42px",
|
||||
fontWeight: "normal",
|
||||
fontSize: "17px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grayMedium.main
|
||||
}
|
||||
}}>
|
||||
Применить
|
||||
</Button>
|
||||
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default ModalPackage;
|
@ -1,127 +1,29 @@
|
||||
import * as React from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import DataGridElement from "./DataGridElement";
|
||||
import ModalMini from "./ModalMini";
|
||||
import ModalPackage from "./ModalPackage";
|
||||
import { ReactNode } from "react";
|
||||
import CustomButton from "./CustomButton";
|
||||
import CustomHeader from "./CustomHeader";
|
||||
import { ArrayProps, Tariff } from "../../../../model/tariff";
|
||||
import { useTariffStore } from "../../../../stores/tariffs";
|
||||
import Cart from "@root/kitUI/Cart/Cart";
|
||||
import { Container, Typography } from "@mui/material";
|
||||
import PrivilegesDG from "./privilegesDG";
|
||||
import TariffsDG from "./tariffsDG";
|
||||
import CreateTariff from "./CreateTariff";
|
||||
import { GridSelectionModel } from "@mui/x-data-grid";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
const ButtonContainer: React.FC<{ children: ReactNode; }> = ({ children }) => {
|
||||
return <Box sx={{
|
||||
marginTop: "35px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gridGap: "20px",
|
||||
marginBottom: "120px",
|
||||
}}>
|
||||
{children}
|
||||
</Box>;
|
||||
};
|
||||
|
||||
const Tariffs: React.FC = () => {
|
||||
const [openModalMini, setOpenModalMini] = React.useState(false);
|
||||
|
||||
const handleOpenModalMini = () => { setOpenModalMini(true); };
|
||||
const handleCloseModalMini = () => { setOpenModalMini(false); };
|
||||
|
||||
const [type, setType] = React.useState(100);
|
||||
const [variant, setVariant] = React.useState(100);
|
||||
const setUpModalMini = (type: number, num: number) => {
|
||||
setType(type);
|
||||
setVariant(num);
|
||||
handleOpenModalMini();
|
||||
};
|
||||
|
||||
const tariffsArray = useTariffStore(state => state.tariffs);
|
||||
const tariffsArraySet = useTariffStore(state => state.setTariffs);
|
||||
const tariffsSelectedRowsData = useTariffStore(state => state.tariffsSelectedRowsData);
|
||||
|
||||
|
||||
|
||||
const [openModalPackage, setOpenModalPackage] = React.useState(false);
|
||||
|
||||
const handleOpenModalPackage = () => { setOpenModalPackage(true); };
|
||||
const handleCloseModalPackage = () => { setOpenModalPackage(false); };
|
||||
|
||||
const newPackage = (name: string) => {
|
||||
const tariffs: Array<Tariff> = [];
|
||||
|
||||
tariffsSelectedRowsData.forEach((item) => {
|
||||
if (item.type === "package" && item.tariffs) {
|
||||
tariffs.push(...item.tariffs);
|
||||
} else {
|
||||
tariffs.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueArray: Array<Tariff> = [];
|
||||
tariffs.forEach((tariff) => {
|
||||
if (uniqueArray.findIndex((a) => a.id === tariff.id) < 0) {
|
||||
uniqueArray.push(tariff);
|
||||
}
|
||||
});
|
||||
|
||||
const packageCreated: ArrayProps = {
|
||||
name,
|
||||
id: new Date().getTime(),
|
||||
type: "package",
|
||||
tariffs: uniqueArray,
|
||||
service: "",
|
||||
disk: 0,
|
||||
time: 0,
|
||||
points: 0,
|
||||
price: 0
|
||||
};
|
||||
|
||||
tariffsArraySet([...tariffsArray, packageCreated]);
|
||||
handleCloseModalPackage();
|
||||
};
|
||||
export default function Tariffs() {
|
||||
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{
|
||||
<Container sx={{
|
||||
width: "90%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<CustomHeader>Шаблонизатор документов</CustomHeader>
|
||||
<ButtonContainer>
|
||||
<CustomButton onClick={() => setUpModalMini(1, 1)}>Создать тариф на время</CustomButton>
|
||||
<CustomButton onClick={() => setUpModalMini(1, 0)}>Создать тариф на объем</CustomButton>
|
||||
<CustomButton onClick={() => setUpModalMini(1, 2)}>Создать тариф на гигабайты</CustomButton>
|
||||
<CustomButton>Изменить тариф</CustomButton>
|
||||
</ButtonContainer>
|
||||
<CustomHeader>Опросник</CustomHeader>
|
||||
<ButtonContainer>
|
||||
<CustomButton sx={{ padding: '11px 43px' }} onClick={() => setUpModalMini(2, 1)}>Создать тариф на время</CustomButton>
|
||||
<CustomButton sx={{ padding: '11px 43px' }} onClick={() => setUpModalMini(2, 0)}>Создать тариф на объем</CustomButton>
|
||||
<CustomButton sx={{ padding: '11px 43px' }}>Изменить тариф </CustomButton>
|
||||
</ButtonContainer>
|
||||
<CustomHeader>Сокращатель ссылок</CustomHeader>
|
||||
<ButtonContainer>
|
||||
<CustomButton onClick={() => setUpModalMini(3, 1)} sx={{ padding: '11px 65px' }}>
|
||||
Создать тариф <br /> на аналитику время
|
||||
</CustomButton>
|
||||
<CustomButton onClick={() => setUpModalMini(3, 1)} sx={{ padding: '11px 65px' }}>
|
||||
Создать тариф <br /> на a/b тесты время
|
||||
</CustomButton>
|
||||
<CustomButton sx={{ padding: '11px 65px' }}>
|
||||
Изменить тариф
|
||||
</CustomButton>
|
||||
</ButtonContainer>
|
||||
<DataGridElement openModal={handleOpenModalPackage} />
|
||||
</Box>
|
||||
<ModalMini open={openModalMini} type={type} variant={variant} close={handleCloseModalMini} />
|
||||
<ModalPackage open={openModalPackage} newPackage={newPackage} close={handleCloseModalPackage} />
|
||||
</>
|
||||
<Typography variant="h6">Список привелегий</Typography>
|
||||
<PrivilegesDG />
|
||||
<CreateTariff />
|
||||
<Typography variant="h6" mt="20px">Список тарифов</Typography>
|
||||
<TariffsDG handleSelectionChange={selectionModel => setSelectedTariffs(selectionModel)} />
|
||||
<Cart selectedTariffs={selectedTariffs} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Tariffs;
|
||||
}
|
33
src/pages/dashboard/Content/Tariffs/privilegesDG.tsx
Normal file
33
src/pages/dashboard/Content/Tariffs/privilegesDG.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import DataGrid from "@kitUI/datagrid";
|
||||
import { usePrivilegeStore } from "@stores/privileges";
|
||||
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'id', width: 40 },
|
||||
{ field: 'name', headerName: 'Привелегия', width: 150 },
|
||||
{ field: 'description', headerName: 'Описание', width: 550 },//инфо из гитлаба.
|
||||
{ field: 'type', headerName: 'Тип', width: 150 },
|
||||
{ field: 'price', headerName: 'Стоимость', width: 50 }
|
||||
];
|
||||
|
||||
export default function PrivilegesDG() {
|
||||
const privileges = usePrivilegeStore(state => state.privileges);
|
||||
|
||||
const privilegesGridData = privileges.map(privilege => ({
|
||||
id: privilege.privilegeId,
|
||||
name: privilege.name,
|
||||
description: privilege.description,
|
||||
type: privilege.type,
|
||||
price: privilege.pricePerUnit,
|
||||
}));
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
// checkboxSelection={true}
|
||||
rows={privilegesGridData}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
48
src/pages/dashboard/Content/Tariffs/tariffsDG.tsx
Normal file
48
src/pages/dashboard/Content/Tariffs/tariffsDG.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import * as React from "react";
|
||||
import { GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
|
||||
import DataGrid from "@kitUI/datagrid";
|
||||
import { useTariffStore } from "@root/stores/tariffs";
|
||||
import { SERVICE_LIST } from "@root/model/tariff";
|
||||
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', width: 100 },
|
||||
{ field: 'name', headerName: 'Название тарифа', width: 150 },
|
||||
{ field: 'serviceName', headerName: 'Сервис', width: 150 },//инфо из гитлаба.
|
||||
{ field: 'privilege', headerName: 'Привелегия', width: 150 },
|
||||
{ field: 'amount', headerName: 'Количество', width: 110 },
|
||||
{ field: 'type', headerName: 'Единица', width: 100 },
|
||||
{ field: 'pricePerUnit', headerName: 'Цена за ед.', width: 100 },
|
||||
{ field: 'isCustomPrice', headerName: 'Кастомная цена', width: 130 },
|
||||
{ field: 'total', headerName: 'Сумма', width: 130 },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
handleSelectionChange: (selectionModel: GridSelectionModel) => void;
|
||||
}
|
||||
|
||||
export default function TariffsDG({ handleSelectionChange }: Props) {
|
||||
const tariffs = useTariffStore(state => state.tariffs);
|
||||
|
||||
const gridData = tariffs.map(tariff => ({
|
||||
id: tariff.id,
|
||||
name: tariff.name,
|
||||
serviceName: SERVICE_LIST.find(service => service.serviceKey === tariff.privilege.serviceKey)?.displayName,
|
||||
privilege: `(${tariff.privilege.privilegeId}) ${tariff.privilege.description}`,
|
||||
amount: tariff.amount,
|
||||
type: tariff.privilege.type === "count" ? "день" : "шт.",
|
||||
pricePerUnit: tariff.customPricePerUnit ?? tariff.privilege.pricePerUnit,
|
||||
isCustomPrice: tariff.customPricePerUnit === undefined ? "Нет" : "Да",
|
||||
total: tariff.amount * (tariff.customPricePerUnit ?? tariff.privilege.pricePerUnit),
|
||||
}));
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
checkboxSelection={true}
|
||||
rows={gridData}
|
||||
columns={columns}
|
||||
components={{ Toolbar: GridToolbar }}
|
||||
onSelectionModelChange={handleSelectionChange}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
import { CartSummary, Discount, Promocode } from "../../../../model/cart";
|
||||
import { ArrayProps, Tariff } from "../../../../model/tariff";
|
||||
|
||||
|
||||
export function calcFitDiscounts(discountsArray: Discount[], discountsActiveArray: number[], cartSummary: { [key: string]: CartSummary; }, fieldAddedValue: string) {
|
||||
const result = discountsActiveArray.filter(e => {
|
||||
const discount = discountsArray[e];
|
||||
const summary = cartSummary[discount.privileges[0].good];
|
||||
return (discount.incomeMore * 100 < parseInt(fieldAddedValue) && discount.incomeMore > 0) ||
|
||||
(discount.toTime < (summary ? summary.days : 0) && discount.toTime > 0 && discount.toCapacity === 0) ||
|
||||
(discount.toCapacity > 0 && discount.toCapacity < (summary ? summary.points : 0) && discount.toTime === 0) ||
|
||||
(discount.toCapacity > 0 && discount.toTime > 0 && discount.toCapacity < (summary ? summary.points : 0) && discount.toTime < (summary ? summary.days : 0)) ||
|
||||
(!discount.toCapacity && !discount.toTime && !discount.incomeMore && !discount.basketMore) ||
|
||||
discount.basketMore;
|
||||
}).filter((e, i, a) => {
|
||||
const discount: Discount = discountsArray[e];
|
||||
if (discount.incomeMore) {
|
||||
return discount.incomeMore === a.reduce((a, e) => Math.max(a, discountsArray[e].incomeMore || 0), 0);
|
||||
}
|
||||
if (discount.toTime && discount.toCapacity) {
|
||||
return discount.toTime === a.reduce((a, e) => Math.max(a, (discountsArray[e].toTime && discountsArray[e].toCapacity) ? discountsArray[e].toTime : 0), 0) && discount.toCapacity === a.reduce((a, e) => Math.max(a, (discountsArray[e].toCapacity && discountsArray[e].toTime) ? discountsArray[e].toCapacity : 0), 0);
|
||||
}
|
||||
if (discount.toTime && !discount.toCapacity) {
|
||||
return discount.toTime === a.reduce((a, e) => Math.max(a, discountsArray[e].toTime && !discountsArray[e].toCapacity ? discountsArray[e].toTime : 0), 0);
|
||||
}
|
||||
if (!discount.toTime && discount.toCapacity) {
|
||||
return discount.toCapacity === a.reduce((a, e) => Math.max(a, discountsArray[e].toCapacity && !discountsArray[e].toTime ? discountsArray[e].toCapacity : 0), 0);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function separator(amount: number) {
|
||||
if (String(amount).length < 4) { return amount; }
|
||||
|
||||
let result: Array<string> = [];
|
||||
const arrs = String(amount).split('.');
|
||||
const arr = arrs[0].split('').reverse();
|
||||
|
||||
arr.forEach((item, i: number) => {
|
||||
result.push(String(arr[i]));
|
||||
if (((i + 1) / 3) - Math.round((i + 1) / 3) === 0) result.push(" ");
|
||||
});
|
||||
|
||||
if (arrs.length > 1) { return result.reverse().join("") + "." + arrs[1]; }
|
||||
else { return result.reverse().join(""); }
|
||||
};
|
||||
|
||||
export function formatPromocodePriveleges(promocode: Promocode) {
|
||||
return promocode.privileges.map(privelege => `${privelege.good} - ${Math.round(privelege.discount * 100)}%`).join(", ");
|
||||
}
|
||||
|
||||
export function calcTotalAndRowData(
|
||||
cartRowsData: ArrayProps[],
|
||||
isNonCommercial: boolean,
|
||||
discountsArray: Discount[],
|
||||
discountsActiveArray: number[],
|
||||
fitDiscounts: number[],
|
||||
addedValueField: string,
|
||||
cartSummary: { [key: string]: CartSummary; },
|
||||
promocode?: Promocode,
|
||||
) {
|
||||
let totalPrice = 0;
|
||||
|
||||
const calculatedCartRowData = cartRowsData.map(cartRow => {
|
||||
let price = cartRow.price;
|
||||
const appliedDiscounts: number[] = [];
|
||||
if (!isNonCommercial) {
|
||||
let percents = 0;
|
||||
if (cartRow.type === "package") {
|
||||
// считаем цену в ПАКЕТАХ
|
||||
price = 0;
|
||||
|
||||
cartRow.tariffs?.forEach((tariff) => {
|
||||
let tariffPrice = tariff.price;
|
||||
percents = 0;
|
||||
|
||||
// применяем скидки по промокоду
|
||||
if (promocode) {
|
||||
promocode.privileges.forEach(privilege => {
|
||||
if (tariff.service === privilege.good) {
|
||||
percents = percents + privilege.discount;
|
||||
}
|
||||
});
|
||||
} else {// применяем активные скидки
|
||||
percents = applyActiveDiscounts(
|
||||
percents,
|
||||
tariff,
|
||||
discountsArray,
|
||||
discountsActiveArray,
|
||||
addedValueField,
|
||||
);
|
||||
}
|
||||
|
||||
// применяем активные скидки по времени объему
|
||||
if (!promocode) {
|
||||
discountsActiveArray.forEach(activeDiscount => {
|
||||
discountsArray.forEach((discount, i) => {
|
||||
if (i === activeDiscount) {
|
||||
if (tariff.time) {
|
||||
const dTime = 0.1;
|
||||
percents = percents + dTime;
|
||||
}
|
||||
|
||||
if (tariff.points) {
|
||||
//const cTime = discountCapacity( tariff.points );
|
||||
//percents = percents + cTime;
|
||||
|
||||
//if( discounts ) discounts += " × ";
|
||||
//if( cTime != 0 ) discounts += `${ Math.round(cTime * 100) }%`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// применяем активные скидки на продукт
|
||||
if (!promocode) {
|
||||
discountsActiveArray.forEach(activeDiscount => {
|
||||
discountsArray.forEach((discount, i) => {
|
||||
if (i === activeDiscount) {
|
||||
if (tariff.time && tariff.points) {
|
||||
// const dProduct = discountProduct( tariff.time, tariff.points );
|
||||
//percents = percents + dProduct;
|
||||
|
||||
//if( discounts ) discounts += " × ";
|
||||
//if( dProduct != 0 ) discounts += `${ Math.round(dProduct * 100) }%`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tariffPrice = tariffPrice - (tariffPrice * percents);
|
||||
|
||||
price += tariffPrice;
|
||||
});
|
||||
} else {
|
||||
// считаем цену в ТАРИФАХ
|
||||
price = cartRow.price;
|
||||
percents = 0;
|
||||
|
||||
// применяем скидки по промокоду
|
||||
if (promocode) {
|
||||
promocode.privileges.forEach(privilege => {
|
||||
if (cartRow.service === privilege.good) {
|
||||
appliedDiscounts.push(privilege.discount);
|
||||
price *= (1 - privilege.discount);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// применяем активные скидки
|
||||
fitDiscounts.forEach(activeDiscount => {
|
||||
const discount = discountsArray[activeDiscount];
|
||||
discount.privileges.forEach((p) => {
|
||||
const svcName = cartRow.service;
|
||||
if (p.good === svcName) {
|
||||
const summary = cartSummary[svcName] || { mbs: 0, points: 0, days: 0 };
|
||||
if (
|
||||
(discount.toCapacity === 0 && discount.toTime === 0 && discount.basketMore === 0 && !(discount.incomeMore)) ||
|
||||
(discount.toCapacity > 0 && summary.points > discount.toCapacity && cartRow.points > 0 && discount.toTime === 0) ||
|
||||
(discount.toTime > 0 && summary.days > discount.toTime * 100 && cartRow.time > 0 && discount.toCapacity === 0) ||
|
||||
(discount.toTime > 0 && discount.toCapacity > 0 && summary.days > discount.toTime * 100 && summary.points > discount.toCapacity)
|
||||
) {
|
||||
price *= (1 - p.discount);
|
||||
appliedDiscounts.push(p.discount);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
percents = Number(percents.toFixed(2));
|
||||
|
||||
price = price - (price * percents);
|
||||
}
|
||||
}
|
||||
totalPrice += price;
|
||||
|
||||
return {
|
||||
...cartRow,
|
||||
price,
|
||||
appliedDiscounts,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
totalPrice,
|
||||
calculatedCartRowData
|
||||
};
|
||||
}
|
||||
|
||||
function applyActiveDiscounts(
|
||||
percents: number,
|
||||
tariff: Tariff,
|
||||
discountsArray: Discount[],
|
||||
discountsActiveArray: number[],
|
||||
addedValueField: string,
|
||||
) {
|
||||
discountsActiveArray.forEach(activeDiscountIndex => {
|
||||
discountsArray[activeDiscountIndex].privileges.forEach((privilege) => {
|
||||
if (privilege.discount !== 0) {
|
||||
if (addedValueField) { // внесено
|
||||
const addedValue = Number(addedValueField);
|
||||
let minDiscount = 100;
|
||||
let minI = -1;
|
||||
discountsArray.forEach((discount, index) => {
|
||||
discount.privileges.forEach((y) => {
|
||||
if (
|
||||
discount.active &&
|
||||
addedValue - y.discount * 100 < minDiscount &&
|
||||
addedValue - y.discount * 100 > 0
|
||||
) {
|
||||
minDiscount = addedValue - y.discount * 100;
|
||||
minI = index;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (minI >= 0) {
|
||||
discountsArray[minI].privileges.forEach((y) => {
|
||||
percents = percents + y.discount / discountsActiveArray.length; // костыль
|
||||
});
|
||||
}
|
||||
} else { // не внесено
|
||||
if (tariff.service === privilege.good) {
|
||||
percents = percents + privilege.discount;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return percents;
|
||||
}
|
||||
|
||||
export function convertTariffs(tariffsArray: ArrayProps[]) {
|
||||
return tariffsArray.map((item) => {
|
||||
if (item.type === "package" && item.tariffs) {
|
||||
const result = item.tariffs.reduce((acc, tariff) => {
|
||||
acc.service = acc.service ? `${acc.service}, ${tariff.service}` : tariff.service;
|
||||
acc.disk = acc.disk + tariff.disk;
|
||||
acc.time = acc.time + tariff.time;
|
||||
acc.points = acc.points + tariff.points;
|
||||
acc.price = acc.price + tariff.price;
|
||||
|
||||
return acc;
|
||||
}, { service: "", disk: "", time: "", points: "", price: 0 });
|
||||
|
||||
return { id: item.id, name: item.name, type: item.type, ...result };
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
@ -13,8 +13,8 @@ import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import { getRoles_mock, TMockData } from "../../../../api/roles";
|
||||
import theme from "../../../../theme"
|
||||
import { getRoles_mock, TMockData } from "../../../api/roles";
|
||||
import theme from "../../../theme"
|
||||
|
||||
|
||||
const Users: React.FC = () => {
|
@ -3,8 +3,8 @@ import { Box } from "@mui/material";
|
||||
import Users from "./Users";
|
||||
import Entities from "./Entities";
|
||||
import Tariffs from "./Tariffs";
|
||||
import Discounts from "./Discounts";
|
||||
import Promocodes from "./Promocode";
|
||||
import DiscountManagement from "./DiscountManagement";
|
||||
import PromocodeManagement from "./PromocodeManagement";
|
||||
|
||||
|
||||
import Support from "./Support";
|
||||
@ -21,8 +21,8 @@ const Content: React.FC<MWProps> = ({ section }) => {
|
||||
<Users />,
|
||||
<Entities />,
|
||||
<Tariffs />,
|
||||
<Discounts />,
|
||||
<Promocodes />,
|
||||
<DiscountManagement />,
|
||||
<PromocodeManagement />,
|
||||
<Error404 />,
|
||||
<Error404 />,
|
||||
<Support />
|
||||
|
@ -1,25 +1,25 @@
|
||||
import create from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
import { ArrayProps } from "../model/tariff";
|
||||
import { CartTotal } from "@root/model/cart";
|
||||
|
||||
|
||||
interface CartStore {
|
||||
cartRowsData: Array<ArrayProps>,
|
||||
setCartRowsData: (array: Array<ArrayProps>) => void,
|
||||
cartTotal: CartTotal | null;
|
||||
setCartTotal: (newCartTotal: CartTotal | null) => void;
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
// persist(
|
||||
(set, get) => ({
|
||||
cartRowsData: [],
|
||||
setCartRowsData: (array: Array<ArrayProps>) => set({ cartRowsData: array }),
|
||||
cartTotal: null,
|
||||
setCartTotal: newCartTotal => set({ cartTotal: newCartTotal })
|
||||
}),
|
||||
{
|
||||
name: "cart-storage",
|
||||
getStorage: () => localStorage,
|
||||
}
|
||||
),
|
||||
// {
|
||||
// name: "cart",
|
||||
// getStorage: () => localStorage,
|
||||
// }
|
||||
// ),
|
||||
{
|
||||
name: "Cart store"
|
||||
}
|
||||
|
@ -1,34 +1,47 @@
|
||||
import create from "zustand";
|
||||
import { GridSelectionModel } from "@mui/x-data-grid";
|
||||
import { AnyDiscount } from "@root/model/cart";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
import { Discount } from "../model/cart";
|
||||
import { testDiscounts } from "./mocks/discounts";
|
||||
import { exampleCartValues } from "./mocks/exampleCartValues";
|
||||
|
||||
|
||||
interface DiscountStore {
|
||||
discountsArray: Array<Discount>,
|
||||
setDiscountsArray: (array: Array<Discount>) => void,
|
||||
discountsActiveArray: Array<number>,
|
||||
setDiscountsActiveArray: (array: Array<number>) => void,
|
||||
discountsSelectedRowsData: Array<Discount>,
|
||||
setDiscountsSelectedRowsData: (array: Array<Discount>) => void,
|
||||
discounts: AnyDiscount[];
|
||||
selectedDiscountIds: GridSelectionModel,
|
||||
addDiscounts: (newDiscounts: AnyDiscount[]) => void;
|
||||
deleteDiscounts: (discountIds: string[]) => void;
|
||||
activateDiscounts: (discountIds: string[]) => void;
|
||||
deactivateDiscounts: (discountIds: string[]) => void;
|
||||
}
|
||||
|
||||
export const useDiscountStore = create<DiscountStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
// persist(
|
||||
(set, get) => ({
|
||||
discountsArray: testDiscounts,
|
||||
setDiscountsArray: (array: Array<Discount>) => set({ discountsArray: array }),
|
||||
discountsActiveArray: [],
|
||||
setDiscountsActiveArray: (array: Array<number>) => set({ discountsActiveArray: array }),
|
||||
discountsSelectedRowsData: [],
|
||||
setDiscountsSelectedRowsData: (array: Array<Discount>) => set({ discountsSelectedRowsData: array }),
|
||||
discounts: exampleCartValues.discounts,
|
||||
selectedDiscountIds: [],
|
||||
addDiscounts: newDiscounts => set(state => ({ discounts: [...state.discounts, ...newDiscounts] })),
|
||||
deleteDiscounts: discountIdsToDelete => set(state => (
|
||||
{ discounts: state.discounts.filter(discount => !discountIdsToDelete.includes(discount._id)) }
|
||||
)),
|
||||
activateDiscounts: discountIds => set(state => {
|
||||
const filteredDiscounts = state.discounts.filter(discount => discountIds.includes(discount._id));
|
||||
|
||||
// TODO activate discounts, use immer js?
|
||||
throw new Error("unimplemented");
|
||||
}),
|
||||
{
|
||||
name: "discount-storage",
|
||||
getStorage: () => localStorage,
|
||||
}
|
||||
),
|
||||
deactivateDiscounts: discountIds => set(state => {
|
||||
const filteredDiscounts = state.discounts.filter(discount => discountIds.includes(discount._id));
|
||||
|
||||
// TODO deactivate discounts, use immer js?
|
||||
throw new Error("unimplemented");
|
||||
}),
|
||||
}),
|
||||
// {
|
||||
// name: "discounts",
|
||||
// getStorage: () => localStorage,
|
||||
// }
|
||||
// ),
|
||||
{
|
||||
name: "Discount store"
|
||||
}
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { Discount } from "../../model/cart";
|
||||
|
||||
export const testDiscounts: Discount[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Скидка 1",
|
||||
endless: false,
|
||||
from: "",
|
||||
dueTo: "",
|
||||
privileges: [
|
||||
{ good: "Опросник", discount: 0.3 },
|
||||
{ good: "Опросник", discount: 0.2 },
|
||||
],
|
||||
active: false,
|
||||
incomeMore: 1,
|
||||
basketMore: 10,
|
||||
toTime: 20,
|
||||
toCapacity: 30,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Скидка 2",
|
||||
endless: false,
|
||||
from: "",
|
||||
dueTo: "",
|
||||
privileges: [
|
||||
{ good: "Опросник", discount: 0.3 },
|
||||
{ good: "Опросник", discount: 0.2 },
|
||||
],
|
||||
active: true,
|
||||
incomeMore: 1,
|
||||
basketMore: 10,
|
||||
toTime: 20,
|
||||
toCapacity: 30,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Скидка 3",
|
||||
endless: false,
|
||||
from: "",
|
||||
dueTo: "",
|
||||
privileges: [
|
||||
{ good: "Опросник", discount: 0.3 },
|
||||
{ good: "Опросник", discount: 0.2 },
|
||||
],
|
||||
active: false,
|
||||
incomeMore: 1,
|
||||
basketMore: 10,
|
||||
toTime: 20,
|
||||
toCapacity: 30,
|
||||
},
|
||||
];
|
1125
src/stores/mocks/exampleCartValues.ts
Normal file
1125
src/stores/mocks/exampleCartValues.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,17 +0,0 @@
|
||||
import { Promocode } from "../../model/cart";
|
||||
|
||||
|
||||
export const testPromocodes: Promocode[] = [
|
||||
{
|
||||
id: 1, name: "Промокод 1", endless: false, from: "", dueTo: "", privileges: [
|
||||
{ good: "Шаблонизатор документов", discount: 0.15 },
|
||||
{ good: "Опросник", discount: 0.3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1, name: "Промокод 2", endless: false, from: "", dueTo: "", privileges: [
|
||||
{ good: "Шаблонизатор документов", discount: 0.4 },
|
||||
{ good: "Опросник", discount: 0.6 }
|
||||
]
|
||||
}
|
||||
];
|
31
src/stores/mocks/tariffs.ts
Normal file
31
src/stores/mocks/tariffs.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Tariff } from "@root/model/tariff";
|
||||
import { exampleCartValues } from "./exampleCartValues";
|
||||
|
||||
|
||||
export const exampleTariffs: Tariff[] = [
|
||||
{
|
||||
id: "tariffId1",
|
||||
name: "Tariff 1",
|
||||
privilege: exampleCartValues.privileges[0],
|
||||
amount: 1000,
|
||||
},
|
||||
{
|
||||
id: "tariffId2",
|
||||
name: "Tariff 2",
|
||||
privilege: exampleCartValues.privileges[1],
|
||||
amount: 2000,
|
||||
customPricePerUnit: 3,
|
||||
},
|
||||
{
|
||||
id: "tariffId3",
|
||||
name: "Tariff 3",
|
||||
privilege: exampleCartValues.privileges[2],
|
||||
amount: 3000,
|
||||
},
|
||||
{
|
||||
id: "tariffId4",
|
||||
name: "Tariff 4",
|
||||
privilege: exampleCartValues.privileges[5],
|
||||
amount: 4000,
|
||||
},
|
||||
]
|
8
src/stores/mocks/user.ts
Normal file
8
src/stores/mocks/user.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { User } from "@root/model/user";
|
||||
|
||||
|
||||
export const testUser: User = {
|
||||
"ID": "buddy",
|
||||
"Type": "",
|
||||
"PurchasesAmount": 11000,
|
||||
};
|
30
src/stores/privileges.ts
Normal file
30
src/stores/privileges.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Privilege } from "@root/model/tariff";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
import { exampleCartValues } from "./mocks/exampleCartValues";
|
||||
|
||||
|
||||
interface PrivilegeStore {
|
||||
privileges: Privilege[];
|
||||
addPrivileges: (newPrivileges: Privilege[]) => void;
|
||||
}
|
||||
|
||||
export const usePrivilegeStore = create<PrivilegeStore>()(
|
||||
devtools(
|
||||
// persist(
|
||||
(set, get) => ({
|
||||
privileges: exampleCartValues.privileges,
|
||||
addPrivileges: newPrivileges => set(state => (
|
||||
{ privileges: [...state.privileges, ...newPrivileges] }
|
||||
)),
|
||||
}),
|
||||
// {
|
||||
// name: "privileges",
|
||||
// getStorage: () => localStorage,
|
||||
// }
|
||||
// ),
|
||||
{
|
||||
name: "Privilege store"
|
||||
}
|
||||
)
|
||||
);
|
@ -1,26 +1,31 @@
|
||||
import create from "zustand";
|
||||
import { Promocode } from "@root/model/cart";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
import { Promocode } from "../model/cart";
|
||||
import { testPromocodes } from "./mocks/promocodes";
|
||||
|
||||
|
||||
interface PromocodeStore {
|
||||
promocodeArray: Array<Promocode>,
|
||||
setPromocodeArray: (array: Array<Promocode>) => void,
|
||||
promocodes: Promocode[];
|
||||
addPromocodes: (newPromocodes: Promocode[]) => void;
|
||||
deletePromocodes: (promocodeIdsToDelete: string[]) => void;
|
||||
}
|
||||
|
||||
export const usePromocodeStore = create<PromocodeStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
// persist(
|
||||
(set, get) => ({
|
||||
promocodeArray: testPromocodes,
|
||||
setPromocodeArray: (array: Array<Promocode>) => set({ promocodeArray: array }),
|
||||
promocodes: [],
|
||||
addPromocodes: newPromocodes => set(state => (
|
||||
{ promocodes: [...state.promocodes, ...newPromocodes] }
|
||||
)),
|
||||
deletePromocodes: promocodeIdsToDelete => set(state => (
|
||||
{ promocodes: state.promocodes.filter(promocode => !promocodeIdsToDelete.includes(promocode.id)) }
|
||||
)),
|
||||
}),
|
||||
{
|
||||
name: "promocode-storage",
|
||||
getStorage: () => localStorage,
|
||||
}
|
||||
),
|
||||
// {
|
||||
// name: "promocodes",
|
||||
// getStorage: () => localStorage,
|
||||
// }
|
||||
// ),
|
||||
{
|
||||
name: "Promocode store"
|
||||
}
|
||||
|
@ -1,29 +1,32 @@
|
||||
import create from "zustand";
|
||||
import { Tariff } from "@root/model/tariff";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
import { ArrayProps } from "../model/tariff";
|
||||
import { exampleTariffs } from "./mocks/tariffs";
|
||||
|
||||
|
||||
interface TariffStore {
|
||||
tariffs: Array<ArrayProps>;
|
||||
setTariffs: (array: Array<ArrayProps>) => void;
|
||||
tariffsSelectedRowsData: Array<ArrayProps>;
|
||||
setTariffsSelectedRowsData: (array: Array<ArrayProps>) => void;
|
||||
tariffs: Tariff[];
|
||||
addTariffs: (newTariffs: Tariff[]) => void;
|
||||
deleteTariffs: (tariffsToDelete: Tariff[]) => void;
|
||||
deleteAllTariffs: () => void;
|
||||
}
|
||||
|
||||
export const useTariffStore = create<TariffStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
// persist(
|
||||
(set, get) => ({
|
||||
tariffs: [],
|
||||
setTariffs: (array: Array<ArrayProps>) => set({ tariffs: array }),
|
||||
tariffsSelectedRowsData: [],
|
||||
setTariffsSelectedRowsData: (array: Array<ArrayProps>) => set({ tariffsSelectedRowsData: array }),
|
||||
tariffs: exampleTariffs,
|
||||
addTariffs: (newTariffs: Tariff[]) => set(state => ({ tariffs: [...state.tariffs, ...newTariffs] })),
|
||||
deleteTariffs: tariffsToDelete => set(state => (
|
||||
{ tariffs: state.tariffs.filter(tariff => !tariffsToDelete.includes(tariff)) }
|
||||
)),
|
||||
deleteAllTariffs: () => set({ tariffs: [] }),
|
||||
}),
|
||||
{
|
||||
name: "tariff-storage",
|
||||
getStorage: () => localStorage,
|
||||
}
|
||||
),
|
||||
// {
|
||||
// name: "tariffs",
|
||||
// getStorage: () => localStorage,
|
||||
// }
|
||||
// ),
|
||||
{
|
||||
name: "Tariff store"
|
||||
}
|
||||
|
181
src/theme.ts
181
src/theme.ts
@ -1,4 +1,6 @@
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import {createTheme, PaletteColorOptions} from "@mui/material";
|
||||
import { deepmerge } from '@mui/utils';
|
||||
//import { createTheme } from "./types";
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
@ -6,17 +8,88 @@ declare module '@mui/material/Button' {
|
||||
enter: true;
|
||||
}
|
||||
}
|
||||
declare module '@mui/material/Paper' {
|
||||
interface PaperPropsVariantOverrides {
|
||||
bar: true;
|
||||
}
|
||||
}
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme {
|
||||
palette: {
|
||||
primary: {
|
||||
main: string
|
||||
},
|
||||
secondary: {
|
||||
main: string;
|
||||
},
|
||||
menu: {
|
||||
main: string;
|
||||
},
|
||||
content: {
|
||||
main: string;
|
||||
},
|
||||
grayLight: {
|
||||
main: string;
|
||||
},
|
||||
grayDark: {
|
||||
main: string;
|
||||
},
|
||||
grayMedium: {
|
||||
main: string;
|
||||
},
|
||||
grayDisabled: {
|
||||
main: string;
|
||||
},
|
||||
golden: {
|
||||
main: string;
|
||||
},
|
||||
goldenDark: {
|
||||
main: string;
|
||||
},
|
||||
goldenMedium: {
|
||||
main: string;
|
||||
},
|
||||
caption: {
|
||||
main: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
menu?: PaletteColorOptions;
|
||||
content?: PaletteColorOptions;
|
||||
grayLight?: PaletteColorOptions;
|
||||
grayDark?: PaletteColorOptions;
|
||||
grayMedium?: PaletteColorOptions;
|
||||
grayDisabled?: PaletteColorOptions;
|
||||
golden?: PaletteColorOptions;
|
||||
goldenDark?: PaletteColorOptions;
|
||||
goldenMedium?: PaletteColorOptions;
|
||||
hover?: PaletteColorOptions;
|
||||
}
|
||||
|
||||
// allow configuration using `createTheme`
|
||||
interface TypographyVariants {
|
||||
body1: React.CSSProperties;
|
||||
subtitle1: React.CSSProperties;
|
||||
subtitle2: React.CSSProperties;
|
||||
caption: React.CSSProperties;
|
||||
h5: React.CSSProperties;
|
||||
h6: React.CSSProperties;
|
||||
button: React.CSSProperties;
|
||||
}
|
||||
}
|
||||
|
||||
const fontFamily: string = "GilroyRegular";
|
||||
const fontWeight: string = "600";
|
||||
|
||||
const theme = createTheme({
|
||||
const options1 = {
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#111217"
|
||||
},
|
||||
secondary: {
|
||||
main: "#ffffff"
|
||||
main: "#e6e8ec"
|
||||
},
|
||||
menu: {
|
||||
main: "#2f3339"
|
||||
@ -24,6 +97,9 @@ const theme = createTheme({
|
||||
content: {
|
||||
main: "#26272c"
|
||||
},
|
||||
hover: {
|
||||
main: "#191a1e"
|
||||
},
|
||||
grayLight: {
|
||||
main: "#707070"
|
||||
},
|
||||
@ -46,7 +122,8 @@ const theme = createTheme({
|
||||
main: "#2a2b1d"
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
const options2 = {
|
||||
typography: {
|
||||
body1: {
|
||||
fontFamily: fontFamily
|
||||
@ -90,91 +167,51 @@ const theme = createTheme({
|
||||
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: options1.palette.secondary.main,
|
||||
backgroundColor: options1.palette.menu.main,
|
||||
padding: "12px",
|
||||
fontSize: "13px",
|
||||
"&:hover": {
|
||||
backgroundColor: options1.palette.hover.main,
|
||||
}
|
||||
}
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: {
|
||||
variant: 'enter' },
|
||||
variant: 'enter'
|
||||
},
|
||||
style: {
|
||||
backgroundColor: "#26272c",
|
||||
color: options1.palette.secondary.main,
|
||||
backgroundColor: options1.palette.content.main,
|
||||
padding: '12px 48px',
|
||||
"&:hover": {
|
||||
backgroundColor: "#2f3339"
|
||||
backgroundColor: options1.palette.hover.main,
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MuiPaper: {
|
||||
variants: [
|
||||
{
|
||||
props: {
|
||||
variant: "bar"
|
||||
},
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme {
|
||||
palette: {
|
||||
primary: {
|
||||
main: string
|
||||
},
|
||||
secondary: {
|
||||
main: string;
|
||||
},
|
||||
menu: {
|
||||
main: string;
|
||||
},
|
||||
content: {
|
||||
main: string;
|
||||
},
|
||||
grayLight: {
|
||||
main: string;
|
||||
},
|
||||
grayDark: {
|
||||
main: string;
|
||||
},
|
||||
grayMedium: {
|
||||
main: string;
|
||||
},
|
||||
grayDisabled: {
|
||||
main: string;
|
||||
},
|
||||
golden: {
|
||||
main: string;
|
||||
},
|
||||
goldenDark: {
|
||||
main: string;
|
||||
},
|
||||
goldenMedium: {
|
||||
main: string;
|
||||
},
|
||||
caption: {
|
||||
main: string;
|
||||
style: {
|
||||
backgroundColor: options1.palette.grayMedium.main,
|
||||
padding: "15px",
|
||||
width: "100%"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
interface PaletteOptions {
|
||||
menu?: PaletteColorOptions;
|
||||
content?: PaletteColorOptions;
|
||||
grayLight?: PaletteColorOptions;
|
||||
grayDark?: PaletteColorOptions;
|
||||
grayMedium?: PaletteColorOptions;
|
||||
grayDisabled?: PaletteColorOptions;
|
||||
golden?: PaletteColorOptions;
|
||||
goldenDark?: PaletteColorOptions;
|
||||
goldenMedium?: PaletteColorOptions;
|
||||
}
|
||||
// allow configuration using `createTheme`
|
||||
interface TypographyVariants {
|
||||
body1: React.CSSProperties;
|
||||
subtitle1: React.CSSProperties;
|
||||
subtitle2: React.CSSProperties;
|
||||
caption: React.CSSProperties;
|
||||
h5: React.CSSProperties;
|
||||
h6: React.CSSProperties;
|
||||
button: React.CSSProperties;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
const theme = createTheme(deepmerge(options1, options2));
|
||||
export default theme;
|
11
tsconfig.extend.json
Normal file
11
tsconfig.extend.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@theme": ["./theme.ts"],
|
||||
"@root/*": ["./*"],
|
||||
"@kitUI/*": ["./kitUI/*"],
|
||||
"@stores/*": ["./stores/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.extend.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
|
Loading…
Reference in New Issue
Block a user