add cart stuff
This commit is contained in:
parent
4c96954764
commit
b0f2e94ebf
@ -15,20 +15,22 @@
|
||||
"registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"zustand": "^4.3.8",
|
||||
"axios": "^1.4.0"
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/react": "^18.2.7",
|
||||
"react": "^18.2.0",
|
||||
"axios": "^1.4.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^5.0.4",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"immer": "^10.0.2",
|
||||
"reconnecting-eventsource": "^1.6.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from "./makeRequest";
|
||||
export * from "./tariff";
|
||||
export * from "./tickets";
|
11
src/api/tariff.ts
Normal file
11
src/api/tariff.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Tariff } from "../model/tariff";
|
||||
import { makeRequest } from "./makeRequest";
|
||||
|
||||
|
||||
export function getTariffById(tariffId:string){
|
||||
return makeRequest<never, Tariff>({
|
||||
url: `https://admin.pena.digital/strator/tariff/${tariffId}`,
|
||||
method: "get",
|
||||
useToken: true,
|
||||
});
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
export * from "./useCart";
|
||||
export * from "./useDebounce";
|
||||
export * from "./useEventListener";
|
||||
export * from "./useSSESubscription";
|
||||
export * from "./useTariffs";
|
||||
export * from "./useThrottle";
|
||||
export * from "./useTicketMessages";
|
||||
export * from "./useTickets";
|
||||
export * from "./useTickets";
|
||||
export * from "./useToken";
|
59
src/hooks/useCart.ts
Normal file
59
src/hooks/useCart.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isAxiosError } from "axios";
|
||||
import { devlog } from "../utils";
|
||||
import { addCartTariffs, removeMissingCartTariffs, setCartTariffStatus, useCartStore } from "../stores/cart";
|
||||
import { Tariff } from "../model/tariff";
|
||||
import { getTariffById } from "../api/tariff";
|
||||
|
||||
|
||||
export function useCart({ tariffs, tariffIds, onTariffRemove }: {
|
||||
tariffs: Tariff[];
|
||||
tariffIds: string[] | null;
|
||||
onTariffRemove: (tariffId: string) => Promise<any>;
|
||||
}) {
|
||||
const onTariffRemoveRef = useRef(onTariffRemove);
|
||||
const cartTariffMap = useCartStore(state => state.cartTariffMap);
|
||||
const cart = useCartStore(state => state.cart);
|
||||
|
||||
useEffect(() => {
|
||||
onTariffRemoveRef.current = onTariffRemove;
|
||||
}, [onTariffRemove]);
|
||||
|
||||
useEffect(function addTariffsToCart() {
|
||||
const knownTariffs: Tariff[] = [];
|
||||
|
||||
tariffIds?.forEach(tariffId => {
|
||||
if (typeof cartTariffMap[tariffId] === "object") return;
|
||||
|
||||
const tariff = tariffs.find(tariff => tariff._id === tariffId);
|
||||
if (tariff) return knownTariffs.push(tariff);
|
||||
|
||||
if (!cartTariffMap[tariffId]) {
|
||||
setCartTariffStatus(tariffId, "loading");
|
||||
|
||||
getTariffById(tariffId).then(tariff => {
|
||||
devlog("Unknown tariff", tariff);
|
||||
addCartTariffs([tariff]);
|
||||
}).catch(error => {
|
||||
devlog(`Error fetching unknown tariff ${tariffId}`, error);
|
||||
setCartTariffStatus(tariffId, "not found");
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
onTariffRemoveRef.current(tariffId).then(() => {
|
||||
devlog(`Unexistant tariff with id ${tariffId} deleted from cart`);
|
||||
}).catch(error => {
|
||||
devlog("Error deleting unexistant tariff from cart", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (knownTariffs.length > 0) addCartTariffs(knownTariffs);
|
||||
}, [tariffIds, cartTariffMap, tariffs]);
|
||||
|
||||
useEffect(function cleanUpCart() {
|
||||
if (tariffIds) removeMissingCartTariffs(tariffIds);
|
||||
}, [tariffIds]);
|
||||
|
||||
return cart;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export * from "./api";
|
||||
export * from "./hooks";
|
||||
export * from "./decorators";
|
||||
export * from "./hooks";
|
||||
export * from "./model";
|
||||
export * from "./stores";
|
||||
export * from "./utils";
|
||||
|
||||
export type * from "./model";
|
19
src/model/cart.ts
Normal file
19
src/model/cart.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export type PrivilegeCartData = {
|
||||
tariffId: string;
|
||||
privilegeId: string;
|
||||
name: string;
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type ServiceCartData = {
|
||||
serviceKey: string;
|
||||
privileges: PrivilegeCartData[];
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type CartData = {
|
||||
services: ServiceCartData[];
|
||||
priceBeforeDiscounts: number;
|
||||
priceAfterDiscounts: number;
|
||||
itemCount: number;
|
||||
};
|
185
src/model/discount.ts
Normal file
185
src/model/discount.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export interface DiscountBase {
|
||||
_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** Этап применения скидки */
|
||||
layer?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export interface PurchasesAmountDiscount extends DiscountBase {
|
||||
conditionType: "purchasesAmount";
|
||||
condition: {
|
||||
purchasesAmount: number;
|
||||
};
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export interface CartPurchasesAmountDiscount extends DiscountBase {
|
||||
conditionType: "cartPurchasesAmount";
|
||||
condition: {
|
||||
cartPurchasesAmount: number;
|
||||
};
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export interface PrivilegeDiscount extends DiscountBase {
|
||||
conditionType: "privilege";
|
||||
condition: {
|
||||
privilege: {
|
||||
id: string;
|
||||
/** Скидка применяется, если значение больше или равно этому значению */
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
target: {
|
||||
products: Array<{
|
||||
privilegeId: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export interface ServiceDiscount extends DiscountBase {
|
||||
conditionType: "service";
|
||||
condition: {
|
||||
service: {
|
||||
id: string;
|
||||
/** Скидка применяется, если значение больше или равно этому значению */
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
target: {
|
||||
service: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export interface UserTypeDiscount extends DiscountBase {
|
||||
conditionType: "userType";
|
||||
condition: {
|
||||
userType: string;
|
||||
};
|
||||
target: {
|
||||
IsAllProducts: boolean;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
};
|
||||
overwhelm: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export interface UserDiscount extends DiscountBase {
|
||||
conditionType: "user";
|
||||
condition: {
|
||||
coupon: string;
|
||||
user: string;
|
||||
};
|
||||
target: {
|
||||
products: Array<{
|
||||
privilegeId: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}>;
|
||||
};
|
||||
overwhelm: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export type AnyDiscount =
|
||||
| PurchasesAmountDiscount
|
||||
| CartPurchasesAmountDiscount
|
||||
| PrivilegeDiscount
|
||||
| ServiceDiscount
|
||||
| UserTypeDiscount
|
||||
| UserDiscount;
|
||||
|
||||
/**
|
||||
* @deprecated Старый тип
|
||||
*/
|
||||
export type DiscountConditionType = AnyDiscount["conditionType"];
|
||||
|
||||
export const SERVICE_LIST = [
|
||||
{
|
||||
serviceKey: "templategen",
|
||||
displayName: "Шаблонизатор документов",
|
||||
},
|
||||
{
|
||||
serviceKey: "squiz",
|
||||
displayName: "Опросник",
|
||||
},
|
||||
{
|
||||
serviceKey: "dwarfener",
|
||||
displayName: "Аналитика сокращателя",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];
|
||||
|
||||
// Актуальный тип скидки
|
||||
export interface Discount {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Layer: number;
|
||||
Description: string;
|
||||
Condition: {
|
||||
Period: {
|
||||
From: string;
|
||||
To: string;
|
||||
};
|
||||
User: string;
|
||||
UserType: string;
|
||||
Coupon: string;
|
||||
PurchasesAmount: number;
|
||||
CartPurchasesAmount: number;
|
||||
Product: string;
|
||||
Term: string;
|
||||
Usage: string;
|
||||
PriceFrom: number;
|
||||
Group: ServiceType;
|
||||
};
|
||||
Target: {
|
||||
Products: [{
|
||||
ID: string;
|
||||
Factor: number;
|
||||
Overhelm: boolean;
|
||||
}];
|
||||
Factor: number;
|
||||
TargetScope: string;
|
||||
TargetGroup: ServiceType;
|
||||
Overhelm: boolean;
|
||||
};
|
||||
Audit: {
|
||||
UpdatedAt: string;
|
||||
CreatedAt: string;
|
||||
DeletedAt?: string;
|
||||
Deleted: boolean;
|
||||
};
|
||||
Deprecated: boolean;
|
||||
};
|
@ -1 +1,5 @@
|
||||
export type * from "./cart";
|
||||
export type * from "./discount";
|
||||
export type * from "./privilege";
|
||||
export type * from "./tariff";
|
||||
export type * from "./ticket";
|
78
src/stores/cart.ts
Normal file
78
src/stores/cart.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { produce } from "immer";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
import { Tariff } from "../model/tariff";
|
||||
import { CartData } from "../model/cart";
|
||||
import { calcCart } from "../utils/calcCart";
|
||||
|
||||
|
||||
interface CartStore {
|
||||
cartTariffMap: Record<string, Tariff | "loading" | "not found">;
|
||||
cart: CartData;
|
||||
notEnoughMoneyAmount: number;
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartStore>()(
|
||||
devtools(
|
||||
(get, set) => ({
|
||||
cartTariffMap: {},
|
||||
cart: {
|
||||
services: [],
|
||||
priceBeforeDiscounts: 0,
|
||||
priceAfterDiscounts: 0,
|
||||
itemCount: 0,
|
||||
},
|
||||
notEnoughMoneyAmount: 0,
|
||||
}),
|
||||
{
|
||||
name: "Cart",
|
||||
enabled: process.env.NODE_ENV === "development",
|
||||
trace: true,
|
||||
actionsBlacklist: "rejected",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const setCartTariffStatus = (tariffId: string, status: "loading" | "not found") => useCartStore.setState(
|
||||
produce<CartStore>(state => {
|
||||
state.cartTariffMap[tariffId] = status;
|
||||
}),
|
||||
false,
|
||||
{
|
||||
type: "setCartTariffStatus",
|
||||
tariffId,
|
||||
status,
|
||||
}
|
||||
);
|
||||
|
||||
export const addCartTariffs = (tariffs: Tariff[]) => useCartStore.setState(
|
||||
produce<CartStore>(state => {
|
||||
tariffs.forEach(tariff => {
|
||||
state.cartTariffMap[tariff._id] = tariff;
|
||||
});
|
||||
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
|
||||
state.cart = calcCart(cartTariffs, []);
|
||||
}),
|
||||
false,
|
||||
{
|
||||
type: tariffs.length > 0 ? "addCartTariffs" : "rejected",
|
||||
tariffIds: tariffs.map(tariff => tariff._id),
|
||||
}
|
||||
);
|
||||
|
||||
export const removeMissingCartTariffs = (tariffIds: string[]) => useCartStore.setState(
|
||||
produce<CartStore>(state => {
|
||||
for (const key in state.cartTariffMap) {
|
||||
if (!tariffIds.includes(key)) delete state.cartTariffMap[key];
|
||||
}
|
||||
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
|
||||
state.cart = calcCart(cartTariffs, []);
|
||||
}),
|
||||
false,
|
||||
{
|
||||
type: "removeMissingCartTariffs",
|
||||
tariffIds,
|
||||
}
|
||||
);
|
||||
|
||||
export const setNotEnoughMoneyAmount = (notEnoughMoneyAmount: number) => useCartStore.setState({ notEnoughMoneyAmount });
|
@ -1 +1,2 @@
|
||||
export * from "./auth";
|
||||
export * from "./auth";
|
||||
export * from "./cart";
|
94
src/utils/calcCart.ts
Normal file
94
src/utils/calcCart.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { CartData, PrivilegeCartData } from "../model/cart";
|
||||
import { AnyDiscount, PrivilegeDiscount, ServiceDiscount } from "../model/discount";
|
||||
import { PrivilegeWithAmount } from "../model/privilege";
|
||||
import { Tariff } from "../model/tariff";
|
||||
|
||||
|
||||
export function calcCart(tariffs: Tariff[], discounts: AnyDiscount[]): CartData {
|
||||
const cartData: CartData = {
|
||||
services: [],
|
||||
priceBeforeDiscounts: 0,
|
||||
priceAfterDiscounts: 0,
|
||||
itemCount: 0,
|
||||
};
|
||||
|
||||
tariffs.forEach(tariff => {
|
||||
if (tariff.price && tariff.price > 0) cartData.priceBeforeDiscounts += tariff.price;
|
||||
|
||||
tariff.privilegies.forEach(privilege => {
|
||||
let serviceData = cartData.services.find(service => service.serviceKey === privilege.serviceKey);
|
||||
if (!serviceData) {
|
||||
serviceData = {
|
||||
serviceKey: privilege.serviceKey,
|
||||
privileges: [],
|
||||
price: 0,
|
||||
};
|
||||
cartData.services.push(serviceData);
|
||||
}
|
||||
|
||||
let privilegePrice = privilege.amount * privilege.price;
|
||||
|
||||
if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice;
|
||||
|
||||
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
|
||||
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
|
||||
|
||||
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts);
|
||||
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
|
||||
|
||||
const privilegeData: PrivilegeCartData = {
|
||||
tariffId: tariff._id,
|
||||
privilegeId: privilege.privilegeId,
|
||||
name: privilege.description,
|
||||
price: privilegePrice,
|
||||
};
|
||||
|
||||
serviceData.privileges.push(privilegeData);
|
||||
serviceData.price += privilegePrice;
|
||||
cartData.priceAfterDiscounts += privilegePrice;
|
||||
cartData.itemCount++;
|
||||
});
|
||||
});
|
||||
|
||||
return cartData;
|
||||
}
|
||||
|
||||
export function findPrivilegeDiscount(privilege: PrivilegeWithAmount, discounts: AnyDiscount[]): PrivilegeDiscount | null {
|
||||
const applicableDiscounts = discounts.filter((discount): discount is PrivilegeDiscount => {
|
||||
return (
|
||||
discount.conditionType === "privilege" &&
|
||||
privilege.privilegeId === discount.condition.privilege.id &&
|
||||
privilege.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;
|
||||
}
|
||||
|
||||
export function findServiceDiscount(
|
||||
serviceKey: string,
|
||||
currentPrice: number,
|
||||
discounts: AnyDiscount[],
|
||||
): ServiceDiscount | null {
|
||||
const discountsForTariffService = discounts.filter((discount): discount is ServiceDiscount => {
|
||||
return (
|
||||
discount.conditionType === "service" &&
|
||||
discount.condition.service.id === serviceKey &&
|
||||
currentPrice >= 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;
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from "./devlog";
|
||||
export * from "./backendMessageHandler";
|
||||
export * from "./backendMessageHandler";
|
||||
export * from "./calcCart";
|
||||
export * from "./devlog";
|
@ -71,6 +71,11 @@ form-data@^4.0.0:
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
immer@^10.0.2:
|
||||
version "10.0.2"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.2.tgz#11636c5b77acf529e059582d76faf338beb56141"
|
||||
integrity sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
Loading…
Reference in New Issue
Block a user