Merge branch 'dev' into 'main'

Dev

See merge request frontend/admin!3
This commit is contained in:
Mikhail 2023-03-11 09:37:35 +00:00
commit 9bfc5d43c9
40 changed files with 5308 additions and 2315 deletions

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"
}
}
]
};

@ -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"
}
}

@ -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

@ -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>,&nbsp;</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>,&nbsp;</span>
}
</span>
))}
&nbsp;
{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

@ -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

@ -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

@ -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

@ -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

@ -0,0 +1,5 @@
export interface User {
ID: string;
Type: "" | "nko";
PurchasesAmount: number;
}

@ -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"
}}>
Скидки: &ensp; {discountText}
</Typography>
<Typography id="transition-modal-title" variant="h6" sx={{
fontWeight: "normal",
textAlign: "center",
marginTop: "10px"
}}>
ИТОГО: &ensp; {resultPrice}
</Typography>
</List>
</Box>
);
}

@ -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" }}>
Привилегии: &ensp;
</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;
}

@ -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}
/>
);
}

@ -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,
},
];

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 }
]
}
];

@ -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

@ -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

@ -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"
}

@ -1,4 +1,6 @@
import { createTheme, PaletteColorOptions } from "@mui/material";
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

@ -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": [

1867
yarn.lock

File diff suppressed because it is too large Load Diff