Merge branch 'dev' into cart-calc
@ -1,34 +1,16 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
}
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "react"],
|
||||
"rules": {
|
||||
"quotes": ["error", "double"]
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"jest": "^29.5.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.3"
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -17,6 +17,25 @@ export type HistoryRecord = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export interface GetHistoryResponse2 {
|
||||
totalPages: number;
|
||||
records: HistoryRecord[];
|
||||
}
|
||||
|
||||
export type HistoryRecord2 = {
|
||||
comment: string;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
isDeleted: boolean;
|
||||
key: string;
|
||||
rawDetails: {
|
||||
price: number;
|
||||
tariffs: Tariff[];
|
||||
};
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
type RawDetails = { Key: string; Value: KeyValue[][] }
|
||||
type KeyValue = { Key: string; Value: string | number };
|
||||
|
||||
@ -25,7 +44,7 @@ const regList:Record<string, number> = {
|
||||
"price": 1
|
||||
}
|
||||
|
||||
export async function getHistory(): Promise<[GetHistoryResponse | null, string?]> {
|
||||
export async function getHistory(): Promise<[GetHistoryResponse | GetHistoryResponse2 | null, string?]> {
|
||||
try {
|
||||
const historyResponse = await makeRequest<never, GetHistoryResponse>({
|
||||
url: process.env.REACT_APP_DOMAIN + "/customer/history?page=1&limit=100&type=payCart",
|
||||
@ -33,6 +52,10 @@ export async function getHistory(): Promise<[GetHistoryResponse | null, string?]
|
||||
useToken: true,
|
||||
})
|
||||
|
||||
if (!Array.isArray(historyResponse.records[0]?.rawDetails)) {
|
||||
return [historyResponse] as [GetHistoryResponse2]
|
||||
}
|
||||
|
||||
const checked = historyResponse.records.map((data) => {
|
||||
console.log(data.rawDetails)
|
||||
const buffer:KeyValue[] = []
|
||||
|
@ -6,7 +6,9 @@ import type { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
|
||||
import type { GetTariffsResponse } from "@root/model/tariff";
|
||||
import { removeTariffFromCart } from "@root/stores/user";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/strator";
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/strator"
|
||||
console.log("домен с которого тарифы запрашиваются", process.env.REACT_APP_DOMAIN)
|
||||
console.log("домен с которого тарифы запрашиваются", apiUrl)
|
||||
|
||||
export async function getTariffs(
|
||||
apiPage: number,
|
||||
|
@ -1,46 +1,46 @@
|
||||
import { makeRequest } from "@frontend/kitui"
|
||||
import { parseAxiosError } from "@root/utils/parse-error"
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "@root/utils/parse-error";
|
||||
|
||||
import { SendTicketMessageRequest } from "@frontend/kitui"
|
||||
import { SendTicketMessageRequest } from "@frontend/kitui";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym"
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym";
|
||||
|
||||
export async function sendTicketMessage(
|
||||
ticketId: string,
|
||||
message: string
|
||||
ticketId: string,
|
||||
message: string
|
||||
): Promise<[null, string?]> {
|
||||
try {
|
||||
const sendTicketMessageResponse = await makeRequest<
|
||||
try {
|
||||
const sendTicketMessageResponse = await makeRequest<
|
||||
SendTicketMessageRequest,
|
||||
null
|
||||
>({
|
||||
url: `${apiUrl}/send`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
|
||||
})
|
||||
url: `${apiUrl}/send`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
|
||||
});
|
||||
|
||||
return [sendTicketMessageResponse]
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError)
|
||||
return [sendTicketMessageResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось отправить сообщение. ${error}`]
|
||||
}
|
||||
return [null, `Не удалось отправить сообщение. ${error}`];
|
||||
}
|
||||
}
|
||||
|
||||
export async function shownMessage(id: string): Promise<[null, string?]> {
|
||||
try {
|
||||
const shownMessageResponse = await makeRequest<{ id: string }, null>({
|
||||
url: apiUrl + "/shown",
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { id },
|
||||
})
|
||||
try {
|
||||
const shownMessageResponse = await makeRequest<{ id: string }, null>({
|
||||
url: apiUrl + "/shown",
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { id },
|
||||
});
|
||||
|
||||
return [shownMessageResponse]
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError)
|
||||
return [shownMessageResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось прочесть сообщение. ${error}`]
|
||||
}
|
||||
return [null, `Не удалось прочесть сообщение. ${error}`];
|
||||
}
|
||||
}
|
||||
|
40
src/assets/Icons/arrowLeft.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export default function ArrowLeft({ color = "#7E2AEA" }: Props) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20.75 12H4.25"
|
||||
stroke={color}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11 5.25L4.25 12L11 18.75"
|
||||
stroke={color}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
55
src/assets/Icons/download.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { Box, SxProps, Theme } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
color: string;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export default function Download({ color, sx }: Props) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "38px",
|
||||
width: "45px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="47"
|
||||
height="42"
|
||||
viewBox="0 0 47 42"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M33.8846 26.8076H44.2692C44.7283 26.8076 45.1685 26.9551 45.4931 27.2177C45.8177 27.4802 46 27.8363 46 28.2076V39.4076C46 39.7789 45.8177 40.135 45.4931 40.3976C45.1685 40.6601 44.7283 40.8076 44.2692 40.8076H2.73077C2.27174 40.8076 1.83151 40.6601 1.50693 40.3976C1.18235 40.135 1 39.7789 1 39.4076V28.2076C1 27.8363 1.18235 27.4802 1.50693 27.2177C1.83151 26.9551 2.27174 26.8076 2.73077 26.8076H13.1154"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M23.5 27V1"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.1155 11.3846L23.5001 1L33.8847 11.3846"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M39.135 36.0776C40.3141 36.0776 41.27 35.1217 41.27 33.9426C41.27 32.7635 40.3141 31.8076 39.135 31.8076C37.9559 31.8076 37 32.7635 37 33.9426C37 35.1217 37.9559 36.0776 39.135 36.0776Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
BIN
src/assets/bank-logo/b2b.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/bank-logo/sberpay.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/bank-logo/spb.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/bank-logo/umaney.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
@ -18,8 +18,6 @@ export default function CustomAccordion({ header, text, divide = false, price, l
|
||||
const upXs = useMediaQuery(theme.breakpoints.up("xs")); //300
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
|
||||
console.log(upXs);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { Typography, Drawer, useMediaQuery, useTheme, Box, IconButton, Badge, Button } from "@mui/material";
|
||||
import { Typography, Drawer, useMediaQuery, useTheme, Box, IconButton, Badge, Button, Alert } from "@mui/material";
|
||||
import SectionWrapper from "./SectionWrapper";
|
||||
import CustomWrapperDrawer from "./CustomWrapperDrawer";
|
||||
import { NotificationsModal } from "./NotificationsModal";
|
||||
@ -22,327 +22,341 @@ import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||
import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart";
|
||||
|
||||
function Drawers() {
|
||||
const [openNotificationsModal, setOpenNotificationsModal] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const bellRef = useRef<HTMLButtonElement | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
|
||||
const cart = useCart();
|
||||
const userAccount = useUserStore((state) => state.userAccount);
|
||||
const tickets = useTicketStore((state) => state.tickets);
|
||||
const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount);
|
||||
const [openNotificationsModal, setOpenNotificationsModal] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const bellRef = useRef<HTMLButtonElement | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
|
||||
const cart = useCart();
|
||||
const userAccount = useUserStore((state) => state.userAccount);
|
||||
const tickets = useTicketStore((state) => state.tickets);
|
||||
const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount);
|
||||
|
||||
const notificationsCount = tickets.filter(
|
||||
({ user, top_message }) => user !== top_message.user_id && top_message.shown.me !== 1
|
||||
).length;
|
||||
const notificationsCount = tickets.filter(
|
||||
({ user, top_message }) => user !== top_message.user_id && top_message.shown.me !== 1
|
||||
).length;
|
||||
|
||||
async function handlePayClick() {
|
||||
setLoading(true);
|
||||
async function handlePayClick() {
|
||||
setLoading(true);
|
||||
|
||||
const [payCartResponse, payCartError] = await payCart();
|
||||
const [payCartResponse, payCartError] = await payCart();
|
||||
|
||||
if (payCartError) {
|
||||
if (payCartError.includes("insufficient funds: ")) {
|
||||
const notEnoughMoneyAmount = parseInt(payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, ""));
|
||||
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
|
||||
}
|
||||
if (payCartError) {
|
||||
if (payCartError.includes("insufficient funds: ")) {
|
||||
const notEnoughMoneyAmount = parseInt(payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, ""));
|
||||
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setLoading(false);
|
||||
|
||||
setIsDrawerOpen(false);
|
||||
navigate("payment")
|
||||
if (!payCartError.includes("insufficient funds: ")) enqueueSnackbar(payCartError);
|
||||
return
|
||||
if (!payCartError.includes("insufficient funds: ")) enqueueSnackbar(payCartError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payCartResponse) {
|
||||
setUserAccount(payCartResponse);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setIsDrawerOpen(false);
|
||||
}
|
||||
|
||||
if (payCartResponse) {
|
||||
setUserAccount(payCartResponse);
|
||||
function handleReplenishWallet() {
|
||||
setIsDrawerOpen(false);
|
||||
navigate("/payment", { state: { notEnoughMoneyAmount } });
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setIsDrawerOpen(false);;
|
||||
}
|
||||
|
||||
function handleReplenishWallet() {
|
||||
navigate("/payment", { state: { notEnoughMoneyAmount } });
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: isTablet ? "10px" : "20px" }}>
|
||||
<IconButton
|
||||
ref={bellRef}
|
||||
aria-label="cart"
|
||||
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderRadius: "6px",
|
||||
background: openNotificationsModal ? theme.palette.purple.main : theme.palette.background.default,
|
||||
"& .MuiBadge-badge": {
|
||||
background: openNotificationsModal ? theme.palette.background.default : theme.palette.purple.main,
|
||||
color: openNotificationsModal ? theme.palette.purple.main : theme.palette.background.default,
|
||||
},
|
||||
"& svg > path:first-of-type": {
|
||||
fill: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
|
||||
},
|
||||
"& svg > path:last-child": {
|
||||
stroke: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
|
||||
},
|
||||
"&:hover": {
|
||||
background: theme.palette.purple.main,
|
||||
"& .MuiBox-root": {
|
||||
background: theme.palette.purple.main,
|
||||
},
|
||||
"& .MuiBadge-badge": {
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.purple.main,
|
||||
},
|
||||
"& svg > path:first-of-type": { fill: "#FFFFFF" },
|
||||
"& svg > path:last-child": { stroke: "#FFFFFF" },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={notificationsCount}
|
||||
sx={{
|
||||
"& .MuiBadge-badge": {
|
||||
display: notificationsCount ? "flex" : "none",
|
||||
color: "#FFFFFF",
|
||||
background: theme.palette.purple.main,
|
||||
transform: "scale(0.8) translate(50%, -50%)",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<BellIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<NotificationsModal
|
||||
open={openNotificationsModal}
|
||||
setOpen={setOpenNotificationsModal}
|
||||
anchorElement={bellRef.current}
|
||||
notifications={tickets
|
||||
.filter(({ user, top_message }) => user !== top_message.user_id)
|
||||
.map((ticket) => ({
|
||||
text: "У вас новое сообщение от техподдержки",
|
||||
date: new Date(ticket.updated_at).toLocaleDateString(),
|
||||
url: `/support/${ticket.id}`,
|
||||
watched: ticket.user === ticket.top_message.user_id || ticket.top_message.shown.me === 1,
|
||||
}))}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => setIsDrawerOpen(true)}
|
||||
component="div"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
background: theme.palette.background.default,
|
||||
borderRadius: "6px",
|
||||
"&:hover": {
|
||||
background: theme.palette.purple.main,
|
||||
"& .MuiBox-root": {
|
||||
background: theme.palette.purple.main,
|
||||
},
|
||||
"& .MuiBadge-badge": {
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.purple.main,
|
||||
},
|
||||
"& svg > path:nth-of-type(1)": { fill: "#FFFFFF" },
|
||||
"& svg > path:nth-of-type(2)": { fill: "#FFFFFF" },
|
||||
"& svg > path:nth-of-type(3)": { stroke: "#FFFFFF" },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={userAccount?.cart.length}
|
||||
sx={{
|
||||
"& .MuiBadge-badge": {
|
||||
display: userAccount?.cart.length ? "flex" : "none",
|
||||
color: "#FFFFFF",
|
||||
background: theme.palette.purple.main,
|
||||
transform: "scale(0.8) translate(50%, -50%)",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CartIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Drawer anchor={"right"} open={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} sx={{ background: "rgba(0, 0, 0, 0.55)" }}>
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
pl: "0px",
|
||||
pr: "0px",
|
||||
width: "450px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
pt: "12px",
|
||||
pb: "12px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
bgcolor: "#F2F3F7",
|
||||
gap: "10px",
|
||||
pl: "20px",
|
||||
pr: "20px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
lineHeight: "21px",
|
||||
font: "Rubick",
|
||||
}}
|
||||
>
|
||||
Корзина
|
||||
</Typography>
|
||||
<IconButton onClick={() => setIsDrawerOpen(false)} sx={{ p: 0 }}>
|
||||
<CrossIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||
{cart.services.map((serviceData) => {
|
||||
return (
|
||||
<CustomWrapperDrawer key={serviceData.serviceKey} serviceData={serviceData} />
|
||||
)})}
|
||||
<Box
|
||||
sx={{
|
||||
mt: "40px",
|
||||
pt: upMd ? "30px" : undefined,
|
||||
borderTop: upMd ? `1px solid ${theme.palette.gray.main}` : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: isTablet ? "10px" : "20px" }}>
|
||||
<IconButton
|
||||
ref={bellRef}
|
||||
aria-label="cart"
|
||||
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)}
|
||||
sx={{
|
||||
width: upMd ? "100%" : undefined,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "column",
|
||||
cursor: "pointer",
|
||||
borderRadius: "6px",
|
||||
background: openNotificationsModal ? theme.palette.purple.main : theme.palette.background.default,
|
||||
"& .MuiBadge-badge": {
|
||||
background: openNotificationsModal ? theme.palette.background.default : theme.palette.purple.main,
|
||||
color: openNotificationsModal ? theme.palette.purple.main : theme.palette.background.default,
|
||||
},
|
||||
"& svg > path:first-of-type": {
|
||||
fill: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
|
||||
},
|
||||
"& svg > path:last-child": {
|
||||
stroke: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
|
||||
},
|
||||
"&:hover": {
|
||||
background: theme.palette.purple.main,
|
||||
"& .MuiBox-root": {
|
||||
background: theme.palette.purple.main,
|
||||
},
|
||||
"& .MuiBadge-badge": {
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.purple.main,
|
||||
},
|
||||
"& svg > path:first-of-type": { fill: "#FFFFFF" },
|
||||
"& svg > path:last-child": { stroke: "#FFFFFF" },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
|
||||
Итоговая цена
|
||||
</Typography>
|
||||
<Typography color={theme.palette.gray.dark} mb={upMd ? "18px" : "30px"}>
|
||||
Здесь написана окончательная стоимость всех услуг сложенных в корзину с учётом всех скидок.
|
||||
</Typography>
|
||||
<Typography color={theme.palette.gray.dark}>
|
||||
После нажатия кнопки оплатить, вы будете перенаправлены на форму оплаты, для оплаты ВСЕЙ корзины (рекомендуем перед оплатой, убрать все лишнее)
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.gray.dark,
|
||||
pb: "100px",
|
||||
pt: "38px",
|
||||
}}
|
||||
>
|
||||
>
|
||||
<Badge
|
||||
badgeContent={
|
||||
cart.priceBeforeDiscounts - cart.priceAfterDiscounts ? (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: "#ff4904",
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
-
|
||||
{`${
|
||||
((cart.priceBeforeDiscounts - cart.priceAfterDiscounts) / (cart.priceBeforeDiscounts / 100)).toFixed(0)
|
||||
}%`}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
color="success"
|
||||
sx={{
|
||||
"& .MuiBadge-dot": {
|
||||
backgroundColor: "#ff4904",
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
},
|
||||
"& .MuiBadge-anchorOriginTopRightRectangle": {
|
||||
backgroundColor: "#ff4904",
|
||||
top: "5px",
|
||||
right: "5px",
|
||||
},
|
||||
|
||||
"& .MuiBadge-anchorOriginTopRightRectangular": {
|
||||
backgroundColor: "#ff4904",
|
||||
height: "31px",
|
||||
padding: "5px 10px",
|
||||
right: "20px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
badgeContent={notificationsCount}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "column" : "row",
|
||||
alignItems: upMd ? "start" : "center",
|
||||
mt: upMd ? "10px" : "30px",
|
||||
gap: "15px",
|
||||
"& .MuiBadge-badge": {
|
||||
display: notificationsCount ? "flex" : "none",
|
||||
color: "#FFFFFF",
|
||||
background: theme.palette.purple.main,
|
||||
transform: "scale(0.8) translate(50%, -50%)",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.orange.main}
|
||||
sx={{
|
||||
textDecoration: "line-through",
|
||||
order: upMd ? 1 : 2,
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(cart.priceBeforeDiscounts / 100)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="p1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "26px",
|
||||
lineHeight: "31px",
|
||||
order: upMd ? 2 : 1,
|
||||
mr: "20px",
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(cart.priceAfterDiscounts / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Badge>
|
||||
<Button
|
||||
disabled = {cart.priceAfterDiscounts === 0}
|
||||
variant="pena-contained-dark"
|
||||
onClick={() => (notEnoughMoneyAmount === 0 ? !loading && handlePayClick() : handleReplenishWallet())}
|
||||
sx={{ mt: "25px", display: "block" }}
|
||||
>
|
||||
{loading ? <Loader size={24} /> : notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionWrapper>
|
||||
</Drawer>
|
||||
</Box>
|
||||
);
|
||||
<BellIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<NotificationsModal
|
||||
open={openNotificationsModal}
|
||||
setOpen={setOpenNotificationsModal}
|
||||
anchorElement={bellRef.current}
|
||||
notifications={tickets
|
||||
.filter(({ user, top_message }) => user !== top_message.user_id)
|
||||
.map((ticket) => ({
|
||||
text: "У вас новое сообщение от техподдержки",
|
||||
date: new Date(ticket.updated_at).toLocaleDateString(),
|
||||
url: `/support/${ticket.id}`,
|
||||
watched: ticket.user === ticket.top_message.user_id || ticket.top_message.shown.me === 1,
|
||||
}))}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => setIsDrawerOpen(true)}
|
||||
component="div"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
background: theme.palette.background.default,
|
||||
borderRadius: "6px",
|
||||
"&:hover": {
|
||||
background: theme.palette.purple.main,
|
||||
"& .MuiBox-root": {
|
||||
background: theme.palette.purple.main,
|
||||
},
|
||||
"& .MuiBadge-badge": {
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.purple.main,
|
||||
},
|
||||
"& svg > path:nth-of-type(1)": { fill: "#FFFFFF" },
|
||||
"& svg > path:nth-of-type(2)": { fill: "#FFFFFF" },
|
||||
"& svg > path:nth-of-type(3)": { stroke: "#FFFFFF" },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={userAccount?.cart.length}
|
||||
sx={{
|
||||
"& .MuiBadge-badge": {
|
||||
display: userAccount?.cart.length ? "flex" : "none",
|
||||
color: "#FFFFFF",
|
||||
background: theme.palette.purple.main,
|
||||
transform: "scale(0.8) translate(50%, -50%)",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CartIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Drawer anchor={"right"} open={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} sx={{ background: "rgba(0, 0, 0, 0.55)" }}>
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
pl: "0px",
|
||||
pr: "0px",
|
||||
width: "450px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
pt: "12px",
|
||||
pb: "12px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
bgcolor: "#F2F3F7",
|
||||
gap: "10px",
|
||||
pl: "20px",
|
||||
pr: "20px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
lineHeight: "21px",
|
||||
font: "Rubick",
|
||||
}}
|
||||
>
|
||||
Корзина
|
||||
</Typography>
|
||||
<IconButton onClick={() => setIsDrawerOpen(false)} sx={{ p: 0 }}>
|
||||
<CrossIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||
{cart.services.map((serviceData) => {
|
||||
return (
|
||||
<CustomWrapperDrawer key={serviceData.serviceKey} serviceData={serviceData} />
|
||||
);
|
||||
})}
|
||||
<Box
|
||||
sx={{
|
||||
mt: "40px",
|
||||
pt: upMd ? "30px" : undefined,
|
||||
borderTop: upMd ? `1px solid ${theme.palette.gray.main}` : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "100%" : undefined,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
|
||||
Итоговая цена
|
||||
</Typography>
|
||||
<Typography color={theme.palette.gray.dark} mb={upMd ? "18px" : "30px"}>
|
||||
Здесь написана окончательная стоимость всех услуг сложенных в корзину с учётом всех скидок.
|
||||
</Typography>
|
||||
<Typography color={theme.palette.gray.dark}>
|
||||
После нажатия кнопки оплатить, вы будете перенаправлены на форму оплаты, для оплаты ВСЕЙ корзины (рекомендуем перед оплатой, убрать все лишнее)
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.gray.dark,
|
||||
pb: "100px",
|
||||
pt: "38px",
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={
|
||||
cart.priceBeforeDiscounts - cart.priceAfterDiscounts ? (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: "#ff4904",
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
-
|
||||
{`${((cart.priceBeforeDiscounts - cart.priceAfterDiscounts) / (cart.priceBeforeDiscounts / 100)).toFixed(0)
|
||||
}%`}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
color="success"
|
||||
sx={{
|
||||
"& .MuiBadge-dot": {
|
||||
backgroundColor: "#ff4904",
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
},
|
||||
"& .MuiBadge-anchorOriginTopRightRectangle": {
|
||||
backgroundColor: "#ff4904",
|
||||
top: "5px",
|
||||
right: "5px",
|
||||
},
|
||||
|
||||
"& .MuiBadge-anchorOriginTopRightRectangular": {
|
||||
backgroundColor: "#ff4904",
|
||||
height: "31px",
|
||||
padding: "5px 10px",
|
||||
right: "20px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "column" : "row",
|
||||
alignItems: upMd ? "start" : "center",
|
||||
mt: upMd ? "10px" : "30px",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.orange.main}
|
||||
sx={{
|
||||
textDecoration: "line-through",
|
||||
order: upMd ? 1 : 2,
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(cart.priceBeforeDiscounts / 100)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="p1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "26px",
|
||||
lineHeight: "31px",
|
||||
order: upMd ? 2 : 1,
|
||||
mr: "20px",
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(cart.priceAfterDiscounts / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Badge>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
mt: "25px",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
{notEnoughMoneyAmount > 0 && (
|
||||
<Alert severity="error" variant="filled">
|
||||
Не хватает {currencyFormatter.format(notEnoughMoneyAmount / 100)}
|
||||
</Alert>
|
||||
)}
|
||||
<Button
|
||||
disabled={cart.priceAfterDiscounts === 0}
|
||||
variant="pena-contained-dark"
|
||||
onClick={() => (notEnoughMoneyAmount === 0 ? !loading && handlePayClick() : handleReplenishWallet())}
|
||||
sx={{ display: "block" }}
|
||||
>
|
||||
{loading ? <Loader size={24} /> : notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionWrapper>
|
||||
</Drawer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default withErrorBoundary(Drawers, {
|
||||
fallback: (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ErrorOutlineIcon color="error" />
|
||||
</Box>
|
||||
),
|
||||
onError: handleComponentError,
|
||||
fallback: (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ErrorOutlineIcon color="error" />
|
||||
</Box>
|
||||
),
|
||||
onError: handleComponentError,
|
||||
});
|
||||
|
@ -10,58 +10,85 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import {
|
||||
addOrUpdateUnauthMessages,
|
||||
useUnauthTicketStore,
|
||||
incrementUnauthMessageApiPage,
|
||||
setUnauthIsPreventAutoscroll,
|
||||
setUnauthSessionData,
|
||||
setIsMessageSending,
|
||||
setUnauthTicketMessageFetchState,
|
||||
} from "@root/stores/unauthTicket";
|
||||
TicketMessage,
|
||||
makeRequest,
|
||||
useTicketsFetcher,
|
||||
useTicketMessages,
|
||||
getMessageFromFetchError,
|
||||
useSSESubscription,
|
||||
createTicket,
|
||||
} from "@frontend/kitui";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ChatMessage from "../ChatMessage";
|
||||
import SendIcon from "../icons/SendIcon";
|
||||
import ArrowLeft from "@root/assets/Icons/arrowLeft";
|
||||
import UserCircleIcon from "./UserCircleIcon";
|
||||
import { throttle } from "@frontend/kitui";
|
||||
import {
|
||||
useTicketMessages,
|
||||
getMessageFromFetchError,
|
||||
useSSESubscription,
|
||||
useEventListener,
|
||||
createTicket,
|
||||
} from "@frontend/kitui";
|
||||
import { sendTicketMessage, shownMessage } from "@root/api/ticket";
|
||||
import { useSSETab } from "@root/utils/hooks/useSSETab";
|
||||
import {
|
||||
checkAcceptableMediaType,
|
||||
MAX_FILE_SIZE,
|
||||
ACCEPT_SEND_MEDIA_TYPES_MAP,
|
||||
} from "@utils/checkAcceptableMediaType";
|
||||
import {
|
||||
useTicketStore,
|
||||
setTicketData,
|
||||
addOrUpdateUnauthMessages,
|
||||
setUnauthTicketMessageFetchState,
|
||||
setUnauthIsPreventAutoscroll,
|
||||
incrementUnauthMessage,
|
||||
setIsMessageSending,
|
||||
} from "@root/stores/tickets";
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import ChatDocument from "./ChatDocument";
|
||||
import ChatImage from "./ChatImage";
|
||||
import ChatVideo from "./ChatVideo";
|
||||
|
||||
import type { WheelEvent, TouchEvent } from "react";
|
||||
|
||||
type ModalWarningType =
|
||||
| "errorType"
|
||||
| "errorSize"
|
||||
| "picture"
|
||||
| "video"
|
||||
| "audio"
|
||||
| "document"
|
||||
| null;
|
||||
interface Props {
|
||||
open: boolean;
|
||||
sx?: SxProps<Theme>;
|
||||
onclickArrow?: () => void;
|
||||
}
|
||||
|
||||
export default function Chat({ open = false, sx }: Props) {
|
||||
export default function Chat({ open = false, onclickArrow, sx }: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(800));
|
||||
const [messageField, setMessageField] = useState<string>("");
|
||||
const sessionData = useUnauthTicketStore((state) => state.sessionData);
|
||||
const messages = useUnauthTicketStore((state) => state.messages);
|
||||
const messageApiPage = useUnauthTicketStore((state) => state.apiPage);
|
||||
const messagesPerPage = useUnauthTicketStore(
|
||||
(state) => state.messagesPerPage
|
||||
);
|
||||
const isMessageSending = useUnauthTicketStore(
|
||||
(state) => state.isMessageSending
|
||||
);
|
||||
const isPreventAutoscroll = useUnauthTicketStore(
|
||||
(state) => state.isPreventAutoscroll
|
||||
);
|
||||
const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId);
|
||||
const fetchState = useUnauthTicketStore(
|
||||
(state) => state.unauthTicketMessageFetchState
|
||||
);
|
||||
const [disableFileButton, setDisableFileButton] = useState(false);
|
||||
const [modalWarningType, setModalWarningType] =
|
||||
useState<ModalWarningType>(null);
|
||||
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const user = useUserStore((state) => state.user?._id);
|
||||
const ticket = useTicketStore(
|
||||
(state) => state[user ? "authData" : "unauthData"]
|
||||
);
|
||||
const {
|
||||
messages,
|
||||
sessionData,
|
||||
isMessageSending,
|
||||
isPreventAutoscroll,
|
||||
lastMessageId,
|
||||
messagesPerPage,
|
||||
unauthTicketMessageFetchState: fetchState,
|
||||
apiPage: messageApiPage,
|
||||
} = ticket;
|
||||
|
||||
const { isActiveSSETab, updateSSEValue } = useSSETab<TicketMessage[]>(
|
||||
"ticket",
|
||||
addOrUpdateUnauthMessages
|
||||
@ -74,8 +101,10 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
messagesPerPage,
|
||||
messageApiPage,
|
||||
onSuccess: useCallback((messages) => {
|
||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1)
|
||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) {
|
||||
chatBoxRef.current.scrollTop = 1;
|
||||
}
|
||||
|
||||
addOrUpdateUnauthMessages(messages);
|
||||
}, []),
|
||||
onError: useCallback((error: Error) => {
|
||||
@ -92,6 +121,7 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
`/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
|
||||
onNewData: (ticketMessages) => {
|
||||
updateSSEValue(ticketMessages);
|
||||
|
||||
addOrUpdateUnauthMessages(ticketMessages);
|
||||
},
|
||||
onDisconnect: useCallback(() => {
|
||||
@ -100,6 +130,34 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
marker: "ticket",
|
||||
});
|
||||
|
||||
useTicketsFetcher({
|
||||
url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets",
|
||||
ticketsPerPage: 10,
|
||||
ticketApiPage: 0,
|
||||
onSuccess: (result) => {
|
||||
if (result.data?.length) {
|
||||
const currentTicket = result.data.find(
|
||||
({ origin }) => !origin.includes("/support")
|
||||
);
|
||||
|
||||
if (!currentTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTicketData({
|
||||
ticketId: currentTicket.id,
|
||||
sessionId: currentTicket.sess,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const message = getMessageFromFetchError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
},
|
||||
onFetchStateChange: () => {},
|
||||
enabled: Boolean(user),
|
||||
});
|
||||
|
||||
const throttledScrollHandler = useMemo(
|
||||
() =>
|
||||
throttle(() => {
|
||||
@ -114,14 +172,12 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
if (fetchState !== "idle") return;
|
||||
|
||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||
incrementUnauthMessageApiPage();
|
||||
incrementUnauthMessage();
|
||||
}
|
||||
}, 200),
|
||||
[fetchState]
|
||||
);
|
||||
|
||||
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
|
||||
|
||||
useEffect(
|
||||
function scrollOnNewMessage() {
|
||||
if (!chatBoxRef.current) return;
|
||||
@ -131,7 +187,6 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
scrollToBottom();
|
||||
}, 50);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[lastMessageId]
|
||||
);
|
||||
@ -146,10 +201,18 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
}
|
||||
}, [open, messages]);
|
||||
|
||||
const loadNewMessages = (
|
||||
event: WheelEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
|
||||
throttledScrollHandler();
|
||||
};
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (!messageField || isMessageSending) return;
|
||||
|
||||
if (!sessionData) {
|
||||
if (!sessionData?.ticketId) {
|
||||
setIsMessageSending(true);
|
||||
createTicket({
|
||||
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
|
||||
@ -157,10 +220,10 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
Title: "Unauth title",
|
||||
Message: messageField,
|
||||
},
|
||||
useToken: false,
|
||||
useToken: Boolean(user),
|
||||
})
|
||||
.then((response) => {
|
||||
setUnauthSessionData({
|
||||
setTicketData({
|
||||
ticketId: response.Ticket,
|
||||
sessionId: response.sess,
|
||||
});
|
||||
@ -210,6 +273,66 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const sendFile = async (file: File) => {
|
||||
if (file === undefined) return true;
|
||||
|
||||
console.log("тут ошибка", modalWarningType);
|
||||
let data;
|
||||
if (!ticket.sessionData?.ticketId) {
|
||||
try {
|
||||
data = await createTicket({
|
||||
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
|
||||
body: {
|
||||
Title: "Unauth title",
|
||||
Message: "",
|
||||
},
|
||||
useToken: Boolean(user),
|
||||
});
|
||||
setTicketData({
|
||||
ticketId: data.Ticket,
|
||||
sessionId: data.sess,
|
||||
});
|
||||
} catch (error: any) {
|
||||
const errorMessage = getMessageFromFetchError(error);
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
}
|
||||
setIsMessageSending(false);
|
||||
}
|
||||
|
||||
const ticketId = ticket.sessionData?.ticketId || data?.Ticket;
|
||||
if (ticketId !== undefined) {
|
||||
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
|
||||
try {
|
||||
const body = new FormData();
|
||||
|
||||
body.append(file.name, file);
|
||||
body.append("ticket", ticketId);
|
||||
await makeRequest({
|
||||
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles",
|
||||
body: body,
|
||||
method: "POST",
|
||||
});
|
||||
} catch (error: any) {
|
||||
const errorMessage = getMessageFromFetchError(error);
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const sendFileHC = async (file: File) => {
|
||||
console.log(file);
|
||||
const check = checkAcceptableMediaType(file);
|
||||
if (check.length > 0) {
|
||||
enqueueSnackbar(check);
|
||||
return;
|
||||
}
|
||||
setDisableFileButton(true);
|
||||
await sendFile(file);
|
||||
setDisableFileButton(false);
|
||||
console.log(disableFileButton);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{open && (
|
||||
@ -217,7 +340,9 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "clamp(250px, calc(100vh - 90px), 600px)",
|
||||
height: isMobile
|
||||
? "100%"
|
||||
: "clamp(250px, calc(100vh - 90px), 600px)",
|
||||
backgroundColor: "#944FEE",
|
||||
borderRadius: "8px",
|
||||
...sx,
|
||||
@ -233,6 +358,11 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
|
||||
}}
|
||||
>
|
||||
{isMobile && (
|
||||
<IconButton onClick={onclickArrow}>
|
||||
<ArrowLeft color="white" />
|
||||
</IconButton>
|
||||
)}
|
||||
<UserCircleIcon />
|
||||
<Box
|
||||
sx={{
|
||||
@ -263,6 +393,8 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onWheel={loadNewMessages}
|
||||
onTouchMove={loadNewMessages}
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -276,16 +408,87 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{sessionData &&
|
||||
messages.map((message) => (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={sessionData.sessionId === message.user_id}
|
||||
/>
|
||||
))}
|
||||
{ticket.sessionData?.ticketId &&
|
||||
messages.map((message) => {
|
||||
const isFileVideo = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType)
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileImage = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType)
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileDocument = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType)
|
||||
);
|
||||
}
|
||||
};
|
||||
if (message.files.length > 0 && isFileImage()) {
|
||||
return (
|
||||
<ChatImage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (message.files.length > 0 && isFileVideo()) {
|
||||
return (
|
||||
<ChatVideo
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (message.files.length > 0 && isFileDocument()) {
|
||||
return (
|
||||
<ChatDocument
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
||||
<InputBase
|
||||
@ -314,6 +517,26 @@ export default function Chat({ open = false, sx }: Props) {
|
||||
onChange={(e) => setMessageField(e.target.value)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
disabled={disableFileButton}
|
||||
onClick={() => {
|
||||
console.log(disableFileButton);
|
||||
if (!disableFileButton) fileInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="fileinput"
|
||||
onChange={({ target }) => {
|
||||
if (target.files?.[0]) {
|
||||
sendFileHC(target.files?.[0]);
|
||||
}
|
||||
}}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
/>
|
||||
<IconButton
|
||||
disabled={isMessageSending}
|
||||
onClick={handleSendMessage}
|
||||
|
113
src/components/FloatingSupportChat/ChatDocument.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { isDateToday } from "../../utils/date";
|
||||
import Download from "@root/assets/Icons/download";
|
||||
|
||||
interface Props {
|
||||
unAuthenticated?: boolean;
|
||||
isSelf: boolean;
|
||||
file: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function ChatDocument({
|
||||
unAuthenticated = false,
|
||||
isSelf,
|
||||
file,
|
||||
createdAt,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
const messageBackgroundColor = isSelf
|
||||
? "white"
|
||||
: unAuthenticated
|
||||
? "#EFF0F5"
|
||||
: "#434657";
|
||||
|
||||
const date = new Date(createdAt);
|
||||
const today = isDateToday(date);
|
||||
const time = date.toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...(!today && {
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "9px",
|
||||
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
alignSelf: "end",
|
||||
fontWeight: 400,
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
order: isSelf ? 1 : 2,
|
||||
margin: isSelf ? "0 0 0 auto" : "0 auto 0 0",
|
||||
color: "#434657",
|
||||
mb: "-4px",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: messageBackgroundColor,
|
||||
border: unAuthenticated
|
||||
? "1px solid #E3E3E3"
|
||||
: `1px solid ${"#434657"}`,
|
||||
order: isSelf ? 2 : 1,
|
||||
p: upMd ? "18px" : "12px",
|
||||
borderRadius: "8px",
|
||||
color: isSelf || unAuthenticated ? "#434657" : "white",
|
||||
position: "relative",
|
||||
maxWidth: `calc(100% - ${today ? 45 : 110}px)`,
|
||||
overflowWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-1px",
|
||||
right: isSelf ? "-8px" : undefined,
|
||||
left: isSelf ? undefined : "-8px",
|
||||
transform: isSelf ? undefined : "scale(-1, 1)",
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="8"
|
||||
viewBox="0 0 16 8"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M0.5 0.5L15.5 0.500007
|
||||
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
|
||||
fill={messageBackgroundColor}
|
||||
stroke={unAuthenticated ? "#E3E3E3" : theme.palette.gray.main}
|
||||
/>
|
||||
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
|
||||
</svg>
|
||||
<Link
|
||||
download=""
|
||||
href={`https://storage.yandexcloud.net/pair/${file}`}
|
||||
style={{
|
||||
color: "#7E2AEA",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<Download color={theme.palette.purple.main} />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
119
src/components/FloatingSupportChat/ChatImage.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
Box,
|
||||
ButtonBase,
|
||||
Link,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { isDateToday } from "../../utils/date";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface Props {
|
||||
unAuthenticated?: boolean;
|
||||
isSelf: boolean;
|
||||
file: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function ChatImage({
|
||||
unAuthenticated = false,
|
||||
isSelf,
|
||||
file,
|
||||
createdAt,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const navigate = useNavigate();
|
||||
const messageBackgroundColor = isSelf
|
||||
? "white"
|
||||
: unAuthenticated
|
||||
? "#EFF0F5"
|
||||
: "#434657";
|
||||
|
||||
const date = new Date(createdAt);
|
||||
const today = isDateToday(date);
|
||||
const time = date.toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...(!today && {
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "9px",
|
||||
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
alignSelf: "end",
|
||||
fontWeight: 400,
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
order: isSelf ? 1 : 2,
|
||||
margin: isSelf ? "0 0 0 auto" : "0 auto 0 0",
|
||||
color: "#434657",
|
||||
mb: "-4px",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: messageBackgroundColor,
|
||||
border: unAuthenticated
|
||||
? "1px solid #E3E3E3"
|
||||
: `1px solid ${"#434657"}`,
|
||||
order: isSelf ? 2 : 1,
|
||||
p: upMd ? "18px" : "12px",
|
||||
borderRadius: "8px",
|
||||
color: isSelf || unAuthenticated ? "#434657" : "white",
|
||||
position: "relative",
|
||||
maxWidth: `calc(100% - ${today ? 45 : 110}px)`,
|
||||
overflowWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-1px",
|
||||
right: isSelf ? "-8px" : undefined,
|
||||
left: isSelf ? undefined : "-8px",
|
||||
transform: isSelf ? undefined : "scale(-1, 1)",
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="8"
|
||||
viewBox="0 0 16 8"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M0.5 0.5L15.5 0.500007
|
||||
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
|
||||
fill={messageBackgroundColor}
|
||||
stroke={unAuthenticated ? "#E3E3E3" : "#434657"}
|
||||
/>
|
||||
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
|
||||
</svg>
|
||||
<ButtonBase target="_blank" href={`/image/${file}`}>
|
||||
<Box
|
||||
component="img"
|
||||
sx={{
|
||||
height: "217px",
|
||||
width: "217px",
|
||||
}}
|
||||
src={`https://storage.yandexcloud.net/pair/${file}`}
|
||||
/>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
125
src/components/FloatingSupportChat/ChatVideo.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import {
|
||||
Box,
|
||||
ButtonBase,
|
||||
Link,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { isDateToday } from "../../utils/date";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface Props {
|
||||
unAuthenticated?: boolean;
|
||||
isSelf: boolean;
|
||||
file: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function ChatImage({
|
||||
unAuthenticated = false,
|
||||
isSelf,
|
||||
file,
|
||||
createdAt,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
() => console.log("delete");
|
||||
});
|
||||
const messageBackgroundColor = isSelf
|
||||
? "white"
|
||||
: unAuthenticated
|
||||
? "#EFF0F5"
|
||||
: "#434657";
|
||||
|
||||
const date = new Date(createdAt);
|
||||
const today = isDateToday(date);
|
||||
const time = date.toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...(!today && {
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "9px",
|
||||
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
alignSelf: "end",
|
||||
fontWeight: 400,
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
order: isSelf ? 1 : 2,
|
||||
margin: isSelf ? "0 0 0 auto" : "0 auto 0 0",
|
||||
color: "#434657",
|
||||
mb: "-4px",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: messageBackgroundColor,
|
||||
border: unAuthenticated
|
||||
? "1px solid #E3E3E3"
|
||||
: `1px solid ${"#434657"}`,
|
||||
order: isSelf ? 2 : 1,
|
||||
p: upMd ? "18px" : "12px",
|
||||
borderRadius: "8px",
|
||||
color: isSelf || unAuthenticated ? "#434657" : "white",
|
||||
position: "relative",
|
||||
maxWidth: `calc(100% - ${today ? 45 : 110}px)`,
|
||||
overflowWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-1px",
|
||||
right: isSelf ? "-8px" : undefined,
|
||||
left: isSelf ? undefined : "-8px",
|
||||
transform: isSelf ? undefined : "scale(-1, 1)",
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="8"
|
||||
viewBox="0 0 16 8"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M0.5 0.5L15.5 0.500007
|
||||
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
|
||||
fill={messageBackgroundColor}
|
||||
stroke={unAuthenticated ? "#E3E3E3" : "#434657"}
|
||||
/>
|
||||
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
|
||||
</svg>
|
||||
<Box
|
||||
component="video"
|
||||
sx={{
|
||||
pointerEvents: "auto",
|
||||
height: "217px",
|
||||
width: "auto",
|
||||
minWidth: "217px",
|
||||
}}
|
||||
controls
|
||||
>
|
||||
<source src={`https://storage.yandexcloud.net/pair/${file}`} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,15 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Fab, Typography, Badge, useTheme } from "@mui/material";
|
||||
import { useState, useEffect, forwardRef } from "react";
|
||||
import {
|
||||
Box,
|
||||
Fab,
|
||||
Typography,
|
||||
Badge,
|
||||
Dialog,
|
||||
Slide,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
|
||||
import CircleDoubleDown from "./CircleDoubleDownIcon";
|
||||
import Chat from "./Chat";
|
||||
|
||||
import { useUnauthTicketStore } from "@root/stores/unauthTicket";
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import { useTicketStore } from "@root/stores/tickets";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import type { TransitionProps } from "@mui/material/transitions";
|
||||
|
||||
const Transition = forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: ReactNode;
|
||||
},
|
||||
ref: React.Ref<unknown>
|
||||
) {
|
||||
console.log(props.children);
|
||||
return (
|
||||
<Slide direction="up" ref={ref} {...props}>
|
||||
{props.children ? (
|
||||
<Box sx={{ height: "100%" }}>{props.children}</Box>
|
||||
) : (
|
||||
<Box />
|
||||
)}
|
||||
</Slide>
|
||||
);
|
||||
});
|
||||
|
||||
export default function FloatingSupportChat() {
|
||||
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
|
||||
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
const { messages } = useUnauthTicketStore((state) => state);
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(800));
|
||||
const user = useUserStore((state) => state.user?._id);
|
||||
const { messages } = useTicketStore(
|
||||
(state) => state[user ? "authData" : "unauthData"]
|
||||
);
|
||||
|
||||
const animation = {
|
||||
"@keyframes runningStripe": {
|
||||
@ -35,6 +71,25 @@ export default function FloatingSupportChat() {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
if (document.fullscreenElement) {
|
||||
setMonitorType(isMobile ? "mobile" : "desktop");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setMonitorType("");
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -49,10 +104,20 @@ export default function FloatingSupportChat() {
|
||||
}}
|
||||
>
|
||||
<Chat
|
||||
open={isChatOpened}
|
||||
open={isChatOpened && (monitorType === "desktop" || !isMobile)}
|
||||
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={isChatOpened && (monitorType === "mobile" || isMobile)}
|
||||
onClose={() => setIsChatOpened(false)}
|
||||
TransitionComponent={Transition}
|
||||
>
|
||||
<Chat
|
||||
open={isChatOpened && (monitorType === "mobile" || isMobile)}
|
||||
onclickArrow={() => setIsChatOpened(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
<Fab
|
||||
disableRipple
|
||||
sx={{
|
||||
|
@ -228,7 +228,7 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
|
||||
variant="pena-contained-dark"
|
||||
sx={{ px: "30px", ml: "40px", width: "245px", mt: "50px" }}
|
||||
>
|
||||
Регистрация / Войти
|
||||
{user ? "Личный кабинет" : "Регистрация / Войти"}
|
||||
</Button>
|
||||
) : (
|
||||
<Box
|
||||
|
@ -21,7 +21,7 @@ export default function NavbarCollapsed({ isLoggedIn }: Props) {
|
||||
component="nav"
|
||||
maxWidth="lg"
|
||||
outerContainerSx={{
|
||||
zIndex: "111111111111",
|
||||
zIndex: "999",
|
||||
position: "fixed",
|
||||
top: "0",
|
||||
backgroundColor: theme.palette.bg.main,
|
||||
|
@ -7,6 +7,7 @@ import Wallet from "./pages/Wallet"
|
||||
import Payment from "./pages/Payment/Payment"
|
||||
import QuizPayment from "./pages/QuizPayment/QuizPayment"
|
||||
import Support from "./pages/Support/Support"
|
||||
import ChatImageNewWindow from "./pages/Support/ChatImageNewWindow"
|
||||
import AccountSettings from "./pages/AccountSettings/AccountSettings"
|
||||
import Landing from "./pages/Landing/Landing"
|
||||
import Tariffs from "./pages/Tariffs/Tariffs"
|
||||
@ -98,6 +99,7 @@ const App = () => {
|
||||
<Route path="/changepwd" element={<Navigate to="/" replace state={{ redirectTo: window.location.pathname + window.location.search }} />} />
|
||||
<Route path="/changepwd/expired" element={<Navigate to="/" replace state={{ redirectTo: "/changepwd/expired" }} />} />
|
||||
|
||||
<Route path={"/image/:srcImage"} element={<ChatImageNewWindow />} />
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route element={<ProtectedLayout />}>
|
||||
<Route path="/tariffs" element={<Tariffs />} />
|
||||
|
@ -1,5 +1,5 @@
|
||||
export interface SendPaymentRequest {
|
||||
type: "bankCard";
|
||||
type: string;
|
||||
currency: string;
|
||||
amount: number;
|
||||
bankCard: {
|
||||
|
@ -37,6 +37,7 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
|
||||
content[0].Value[0].forEach((item) => {
|
||||
valuesByKey[item.Key] = item.Value
|
||||
})
|
||||
console.log("Я врапер")
|
||||
console.log(content)
|
||||
console.log(content[0])
|
||||
console.log(content[0].Value)
|
||||
|
233
src/pages/History/AccordionWrapper2.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import CustomAccordion from "@components/CustomAccordion";
|
||||
import File from "@components/icons/File";
|
||||
import { getDeclension } from "@utils/declension";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { addTariffToCart } from "@root/stores/user";
|
||||
import { Tariff } from "@frontend/kitui";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
|
||||
export type History = {
|
||||
title: string;
|
||||
date: string;
|
||||
info: string;
|
||||
description: string;
|
||||
payMethod?: string;
|
||||
expired?: boolean;
|
||||
};
|
||||
|
||||
interface AccordionWrapperProps {
|
||||
tariff: Tariff;
|
||||
price: number;
|
||||
last?: boolean;
|
||||
first?: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function AccordionWrapper2({ tariff, price, last, first, createdAt }: AccordionWrapperProps) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(900));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(560));
|
||||
|
||||
async function handleTariffItemClick(tariffId: string) {
|
||||
const { patchCartError } = await addTariffToCart(tariffId);
|
||||
if (patchCartError) {
|
||||
enqueueSnackbar(patchCartError);
|
||||
} else {
|
||||
enqueueSnackbar("Тариф добавлен в корзину");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
}}
|
||||
>
|
||||
<CustomAccordion
|
||||
last={last}
|
||||
first={first}
|
||||
divide
|
||||
text={tariff.privileges.map(privilege => (
|
||||
`${privilege.description} - ${privilege.serviceKey} ${getDeclension(Number(privilege.serviceKey), privilege.value)}`
|
||||
))}
|
||||
header={
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: upMd ? "72px" : undefined,
|
||||
padding: "20px 20px 20px 0",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
gap: "20px",
|
||||
alignItems: upSm ? "center" : undefined,
|
||||
flexDirection: upSm ? undefined : "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: upSm ? "center" : undefined,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: upSm ? undefined : "column",
|
||||
gap: upMd ? "51px" : "10px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
width: "110px",
|
||||
fontSize: upMd ? "20px" : "18px",
|
||||
lineHeight: upMd ? undefined : "19px",
|
||||
fontWeight: 500,
|
||||
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.text.secondary,
|
||||
px: 0,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{createdAt}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
title={tariff.isCustom ? "Мой тариф" : tariff.name}
|
||||
sx={{
|
||||
fontSize: upMd ? "18px" : "16px",
|
||||
lineHeight: upMd ? undefined : "19px",
|
||||
fontWeight: 500,
|
||||
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.gray.dark,
|
||||
px: 0,
|
||||
width: "200px",
|
||||
maxWidth: "200px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{tariff.isCustom ? "Мой тариф" : tariff.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexFlow: "1",
|
||||
flexBasis: "60%",
|
||||
}}
|
||||
>
|
||||
<Box display="flex" width="100%" justifyContent="space-between">
|
||||
{/* <Typography
|
||||
sx={{
|
||||
display: upMd ? undefined : "none",
|
||||
fontSize: upMd ? "18px" : "16px",
|
||||
lineHeight: upMd ? undefined : "19px",
|
||||
fontWeight: 400,
|
||||
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
|
||||
px: 0,
|
||||
}}
|
||||
>
|
||||
{valuesByKey.payMethod && <Typography
|
||||
sx={{
|
||||
maxWidth: "300px",
|
||||
width: "300px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>Способ оплаты: {valuesByKey.payMethod}</Typography>}
|
||||
</Typography> */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
gap: upSm ? "111px" : "17px",
|
||||
width: "100%",
|
||||
maxWidth: isTablet ? null : "160px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
marginLeft: isTablet ? (isMobile ? null : "auto") : null,
|
||||
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.gray.dark,
|
||||
fontSize: upSm ? "20px" : "16px",
|
||||
fontWeight: 500,
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(price)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
{!isMobile &&
|
||||
<>
|
||||
{/* <IconButton onClick={onClickMail}>
|
||||
<EmailIcon fontSize={"large"}/>
|
||||
</IconButton> */}
|
||||
<IconButton
|
||||
title="Добавить в корзину тариф"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTariffItemClick(tariff._id);
|
||||
}}
|
||||
sx={{
|
||||
ml: "20px",
|
||||
bgcolor: "#EEE4FC",
|
||||
stroke: "#7E2AEA",
|
||||
borderRadius: 2,
|
||||
"&:hover": {
|
||||
bgcolor: "#7E2AEA",
|
||||
stroke: "white",
|
||||
},
|
||||
"&:active": {
|
||||
bgcolor: "black",
|
||||
stroke: "white",
|
||||
}
|
||||
}}
|
||||
>
|
||||
<File></File>
|
||||
</IconButton>
|
||||
</>
|
||||
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
{isMobile &&
|
||||
<>
|
||||
{/* <IconButton onClick={onClickMail}>
|
||||
<EmailIcon fontSize={"large"}/>
|
||||
</IconButton> */}
|
||||
<IconButton
|
||||
title="Добавить в корзину тариф"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTariffItemClick(tariff._id);
|
||||
}}
|
||||
sx={{
|
||||
mr: "10px",
|
||||
bgcolor: "#EEE4FC",
|
||||
stroke: "#7E2AEA",
|
||||
borderRadius: 2,
|
||||
"&:hover": {
|
||||
bgcolor: "#7E2AEA",
|
||||
stroke: "white",
|
||||
},
|
||||
"&:active": {
|
||||
bgcolor: "black",
|
||||
stroke: "white",
|
||||
}
|
||||
}}
|
||||
>
|
||||
<File></File>
|
||||
</IconButton>
|
||||
</>
|
||||
|
||||
}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,121 +1,150 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack"
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
|
||||
import SectionWrapper from "@root/components/SectionWrapper"
|
||||
import { Select } from "@root/components/Select"
|
||||
import { Tabs } from "@root/components/Tabs"
|
||||
import SectionWrapper from "@root/components/SectionWrapper";
|
||||
import { Select } from "@root/components/Select";
|
||||
import { Tabs } from "@root/components/Tabs";
|
||||
|
||||
import AccordionWrapper from "./AccordionWrapper"
|
||||
import { HISTORY } from "./historyMocks"
|
||||
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"
|
||||
import { useHistoryData } from "@root/utils/hooks/useHistoryData"
|
||||
import { isArray } from "cypress/types/lodash"
|
||||
import { ErrorBoundary } from "react-error-boundary"
|
||||
import { handleComponentError } from "@root/utils/handleComponentError"
|
||||
import AccordionWrapper from "./AccordionWrapper";
|
||||
import { HISTORY } from "./historyMocks";
|
||||
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
|
||||
import { useHistoryData } from "@root/utils/hooks/useHistoryData";
|
||||
import { isArray } from "cypress/types/lodash";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||
import { useHistoryStore } from "@root/stores/history";
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import {enqueueSnackbar} from "notistack"
|
||||
import { makeRequest } from "@frontend/kitui"
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { HistoryRecord, HistoryRecord2 } from "@root/api/history";
|
||||
import AccordionWrapper2 from "./AccordionWrapper2";
|
||||
|
||||
const subPages = ["Платежи"]
|
||||
const subPages = ["Платежи"];
|
||||
// const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]
|
||||
|
||||
export default function History() {
|
||||
const [selectedItem, setSelectedItem] = useState<number>(0)
|
||||
const [selectedItem, setSelectedItem] = useState<number>(0);
|
||||
|
||||
const theme = useTheme()
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"))
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600))
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
|
||||
const historyData = useHistoryStore(state => state.history)
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const historyData = useHistoryStore(state => state.history);
|
||||
const handleCustomBackNavigation = useHistoryTracker();
|
||||
|
||||
const handleCustomBackNavigation = useHistoryTracker()
|
||||
const extractDateFromString = (tariffName: string) => {
|
||||
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/);
|
||||
return dateMatch ? dateMatch[0] : "";
|
||||
};
|
||||
|
||||
const extractDateFromString = (tariffName: string) => {
|
||||
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/)
|
||||
return dateMatch ? dateMatch[0] : ""
|
||||
}
|
||||
async function handleHistoryResponse(tariffId: string) {
|
||||
try {
|
||||
await makeRequest(
|
||||
{
|
||||
url: process.env.REACT_APP_DOMAIN + `/customer/sendReport/${tariffId}`,
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
enqueueSnackbar("Запрос отправлен");
|
||||
} catch (e) {
|
||||
enqueueSnackbar("извините, произошла ошибка");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleHistoryResponse(tariffId: string) {
|
||||
try {
|
||||
await makeRequest (
|
||||
{
|
||||
url: process.env.REACT_APP_DOMAIN + `/customer/sendReport/${tariffId}`,
|
||||
method: "POST",
|
||||
}
|
||||
)
|
||||
enqueueSnackbar("Запрос отправлен")
|
||||
} catch (e) {
|
||||
enqueueSnackbar("извините, произошла ошибка")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: upMd ? "25px" : "20px",
|
||||
mb: upMd ? "70px" : "37px",
|
||||
px: isTablet ? (isTablet ? "18px" : "40px") : "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: isTablet ? "38px" : "20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{isMobile && (
|
||||
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: isMobile ? "24px" : "36px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
История
|
||||
</Typography>
|
||||
</Box>
|
||||
{isMobile ? (
|
||||
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
||||
) : (
|
||||
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
||||
)}
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography mt="8px">Ошибка загрузки истории</Typography>
|
||||
}
|
||||
onError={handleComponentError}
|
||||
>
|
||||
{historyData?.length === 0 && <Typography textAlign="center" >Нет данных</Typography>}
|
||||
{historyData?.filter((e) => {
|
||||
e.createdAt = extractDateFromString(e.createdAt)
|
||||
return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value)
|
||||
)})
|
||||
.map(( e, index) => {
|
||||
return (
|
||||
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
|
||||
<AccordionWrapper
|
||||
first={index === 0}
|
||||
last={index === historyData?.length - 1}
|
||||
content={e.rawDetails}
|
||||
key={e.id}
|
||||
createdAt={e.createdAt}
|
||||
onClickMail={(event: any)=>{
|
||||
event.stopPropagation()
|
||||
handleHistoryResponse(e.id)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)})}
|
||||
</ErrorBoundary>
|
||||
</SectionWrapper>
|
||||
)
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: upMd ? "25px" : "20px",
|
||||
mb: upMd ? "70px" : "37px",
|
||||
px: isTablet ? (isTablet ? "18px" : "40px") : "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: isTablet ? "38px" : "20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{isMobile && (
|
||||
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: isMobile ? "24px" : "36px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
История
|
||||
</Typography>
|
||||
</Box>
|
||||
{isMobile ? (
|
||||
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
||||
) : (
|
||||
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
||||
)}
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography mt="8px">Ошибка загрузки истории</Typography>
|
||||
}
|
||||
onError={handleComponentError}
|
||||
>
|
||||
{historyData?.length === 0 && <Typography textAlign="center">Нет данных</Typography>}
|
||||
{/* Для ненормального rawDetails */}
|
||||
{historyData?.filter((e): e is HistoryRecord => {
|
||||
e.createdAt = extractDateFromString(e.createdAt);
|
||||
return (
|
||||
!e.isDeleted
|
||||
&& e.key === "payCart"
|
||||
&& Array.isArray(e.rawDetails)
|
||||
&& Array.isArray(e.rawDetails[0].Value)
|
||||
);
|
||||
}).map((e, index) => {
|
||||
return (
|
||||
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
|
||||
<AccordionWrapper
|
||||
first={index === 0}
|
||||
last={index === historyData?.length - 1}
|
||||
content={(e as HistoryRecord).rawDetails}
|
||||
key={e.id}
|
||||
createdAt={e.createdAt}
|
||||
onClickMail={(event: any) => {
|
||||
event.stopPropagation();
|
||||
handleHistoryResponse(e.id);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{/* Для нормального rawDetails */}
|
||||
{historyData?.filter((e): e is HistoryRecord2 => {
|
||||
e.createdAt = extractDateFromString(e.createdAt);
|
||||
return (
|
||||
!e.isDeleted
|
||||
&& e.key === "payCart"
|
||||
&& !Array.isArray(e.rawDetails)
|
||||
&& !!e.rawDetails.tariffs[0]
|
||||
);
|
||||
}).map((e, index) => {
|
||||
return (
|
||||
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
|
||||
<AccordionWrapper2
|
||||
key={e.id}
|
||||
first={index === 0}
|
||||
last={index === historyData?.length - 1}
|
||||
createdAt={e.createdAt}
|
||||
tariff={e.rawDetails.tariffs[0]}
|
||||
price={e.rawDetails.price}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -9,10 +9,10 @@ import {
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import SectionWrapper from "@components/SectionWrapper";
|
||||
import PaymentMethodCard from "./PaymentMethodCard";
|
||||
import mastercardLogo from "@root/assets/bank-logo/logo-mastercard.png";
|
||||
import visaLogo from "@root/assets/bank-logo/logo-visa.png";
|
||||
import qiwiLogo from "@root/assets/bank-logo/logo-qiwi.png";
|
||||
import mirLogo from "@root/assets/bank-logo/logo-mir.png";
|
||||
import umoneyLogo from "@root/assets/bank-logo/umaney.png";
|
||||
import b2bLogo from "@root/assets/bank-logo/b2b.png";
|
||||
import spbLogo from "@root/assets/bank-logo/spb.png";
|
||||
import sberpayLogo from "@root/assets/bank-logo/sberpay.png";
|
||||
import tinkoffLogo from "@root/assets/bank-logo/logo-tinkoff.png";
|
||||
import rsPayLogo from "@root/assets/bank-logo/rs-pay.png";
|
||||
import { cardShadow } from "@root/utils/theme";
|
||||
@ -37,11 +37,11 @@ type PaymentMethod = {
|
||||
};
|
||||
|
||||
const paymentMethods: PaymentMethod[] = [
|
||||
{ label: "Mastercard", name: "mastercard", image: mastercardLogo },
|
||||
{ label: "Visa", name: "visa", image: visaLogo },
|
||||
{ label: "QIWI Кошелек", name: "qiwi", image: qiwiLogo },
|
||||
{ label: "Мир", name: "mir", image: mirLogo },
|
||||
{ label: "Тинькофф", name: "tinkoff", image: tinkoffLogo },
|
||||
{ label: "Тинькофф", name: "tinkoffBank", image: tinkoffLogo },
|
||||
{ label: "СБП", name: "sbp", image: spbLogo },
|
||||
{ label: "SberPay", name: "sberbank", image: sberpayLogo },
|
||||
{ label: "B2B Сбербанк", name: "b2bSberbank", image: b2bLogo },
|
||||
{ label: "ЮMoney", name: "yoomoney", image: umoneyLogo },
|
||||
];
|
||||
|
||||
type PaymentMethodType = (typeof paymentMethods)[number]["name"];
|
||||
@ -53,7 +53,7 @@ export default function Payment() {
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] =
|
||||
useState<PaymentMethodType | null>("rspay");
|
||||
useState<PaymentMethodType | null>("");
|
||||
const [warnModalOpen, setWarnModalOpen] = useState<boolean>(false);
|
||||
const [sorryModalOpen, setSorryModalOpen] = useState<boolean>(false);
|
||||
const [paymentValueField, setPaymentValueField] = useState<string>("0");
|
||||
@ -90,14 +90,56 @@ export default function Payment() {
|
||||
}
|
||||
|
||||
if (Number(paymentValueField) === 0) {
|
||||
enqueueSnackbar("Введите сумму")
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod !== "rspay") {
|
||||
const [sendPaymentResponse, sendPaymentError] = await sendPayment({
|
||||
fromSquiz,
|
||||
fromSquiz,
|
||||
body: {
|
||||
type: selectedPaymentMethod,
|
||||
amount: Number(paymentValueField) * 100,
|
||||
currency: "RUB",
|
||||
bankCard: {
|
||||
number: "RUB",
|
||||
expiryYear: "2021",
|
||||
expiryMonth: "05",
|
||||
csc: "05",
|
||||
cardholder: "IVAN IVANOV",
|
||||
},
|
||||
phoneNumber: "79000000000",
|
||||
login: "login_test",
|
||||
returnUrl: window.location.origin + "/wallet",
|
||||
},
|
||||
});
|
||||
|
||||
if (selectedPaymentMethod === "rspay") {
|
||||
if (verificationStatus !== VerificationStatus.VERIFICATED) {
|
||||
setWarnModalOpen(true);
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(paymentValueField)
|
||||
if (Number(paymentValueField) < 900){
|
||||
enqueueSnackbar("Минимальная сумма 900р")
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sendRSPaymentError = await sendRSPayment(Number(paymentValueField));
|
||||
|
||||
if (sendRSPaymentError) {
|
||||
return enqueueSnackbar(sendRSPaymentError);
|
||||
}
|
||||
|
||||
enqueueSnackbar(
|
||||
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
|
||||
);
|
||||
|
||||
navigate("/settings");
|
||||
}
|
||||
|
||||
if (sendPaymentError) {
|
||||
return enqueueSnackbar(sendPaymentError);
|
||||
}
|
||||
@ -107,6 +149,31 @@ export default function Payment() {
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
if (verificationStatus !== VerificationStatus.VERIFICATED) {
|
||||
setWarnModalOpen(true);
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(paymentValueField)
|
||||
if (Number(paymentValueField) < 900){
|
||||
enqueueSnackbar("Минимальная сумма 900р")
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sendRSPaymentError = await sendRSPayment(Number(paymentValueField));
|
||||
|
||||
if (sendRSPaymentError) {
|
||||
return enqueueSnackbar(sendRSPaymentError);
|
||||
}
|
||||
|
||||
enqueueSnackbar(
|
||||
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
|
||||
);
|
||||
|
||||
navigate("/settings");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -167,46 +234,22 @@ export default function Payment() {
|
||||
>
|
||||
{paymentMethods.map(({ name, label, image, unpopular = false }) => (
|
||||
<PaymentMethodCard
|
||||
isSelected={false}
|
||||
isSelected={selectedPaymentMethod === name}
|
||||
key={name}
|
||||
label={label}
|
||||
image={image}
|
||||
onClick={() => {
|
||||
setSorryModalOpen(true)
|
||||
// setSelectedPaymentMethod(name)
|
||||
setSelectedPaymentMethod(name)
|
||||
}}
|
||||
unpopular={true}
|
||||
unpopular={false}
|
||||
/>
|
||||
))}
|
||||
<PaymentMethodCard
|
||||
isSelected={false}
|
||||
isSelected={selectedPaymentMethod === "rspay"}
|
||||
label={"Расчётный счёт"}
|
||||
image={rsPayLogo}
|
||||
onClick={async() => {
|
||||
|
||||
if (verificationStatus !== VerificationStatus.VERIFICATED) {
|
||||
setWarnModalOpen(true);
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(paymentValueField)
|
||||
if (Number(paymentValueField) < 900){
|
||||
enqueueSnackbar("Минимальная сумма 900р")
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sendRSPaymentError = await sendRSPayment(Number(paymentValueField));
|
||||
|
||||
if (sendRSPaymentError) {
|
||||
return enqueueSnackbar(sendRSPaymentError);
|
||||
}
|
||||
|
||||
enqueueSnackbar(
|
||||
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
|
||||
);
|
||||
|
||||
navigate("/settings");
|
||||
setSelectedPaymentMethod("rspay")
|
||||
}}
|
||||
unpopular={false}
|
||||
/>
|
||||
@ -262,49 +305,7 @@ export default function Payment() {
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="pena-outlined-light"
|
||||
onClick={async () => {
|
||||
|
||||
|
||||
if (verificationStatus !== VerificationStatus.VERIFICATED) {
|
||||
setWarnModalOpen(true);
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(paymentValueField)
|
||||
if (Number(paymentValueField) < 900){
|
||||
enqueueSnackbar("Минимальная сумма 900р")
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sendRSPaymentError = await sendRSPayment(Number(paymentValueField));
|
||||
|
||||
if (sendRSPaymentError) {
|
||||
return enqueueSnackbar(sendRSPaymentError);
|
||||
}
|
||||
|
||||
enqueueSnackbar(
|
||||
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
|
||||
);
|
||||
|
||||
navigate("/settings");
|
||||
|
||||
}}
|
||||
sx={{
|
||||
mt: "auto",
|
||||
color: "black",
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
border: `1px solid ${theme.palette.purple.dark}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Оплатить
|
||||
</Button>
|
||||
{/* {paymentLink ? (
|
||||
{paymentLink ? (
|
||||
<Button
|
||||
variant="pena-outlined-light"
|
||||
component="a"
|
||||
@ -340,7 +341,7 @@ export default function Payment() {
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
)} */}
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<WarnModal open={warnModalOpen} setOpen={setWarnModalOpen} />
|
||||
|
20
src/pages/Support/ChatImageNewWindow.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export default function ChatImageNewWindow() {
|
||||
const location = useLocation();
|
||||
console.log(location);
|
||||
const srcImage = location.pathname.split("image/")[1];
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
component="img"
|
||||
sx={{
|
||||
maxHeight: "100vh",
|
||||
maxWidth: "100vw",
|
||||
}}
|
||||
src={`https://storage.yandexcloud.net/pair/${srcImage}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -14,7 +14,7 @@ import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import SendIcon from "@components/icons/SendIcon";
|
||||
import { throttle, useToken } from "@frontend/kitui";
|
||||
import { makeRequest, throttle, useToken } from "@frontend/kitui";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useTicketStore } from "@root/stores/tickets";
|
||||
import {
|
||||
@ -38,8 +38,18 @@ import { shownMessage, sendTicketMessage } from "@root/api/ticket";
|
||||
import { withErrorBoundary } from "react-error-boundary";
|
||||
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||
import { useSSETab } from "@root/utils/hooks/useSSETab";
|
||||
import {
|
||||
checkAcceptableMediaType,
|
||||
MAX_FILE_SIZE,
|
||||
ACCEPT_SEND_MEDIA_TYPES_MAP,
|
||||
} from "@utils/checkAcceptableMediaType";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import ChatDocument from "@components/FloatingSupportChat/ChatDocument";
|
||||
import ChatImage from "@components/FloatingSupportChat/ChatImage";
|
||||
import ChatVideo from "@components/FloatingSupportChat/ChatVideo";
|
||||
|
||||
function SupportChat() {
|
||||
console.log("сапортчат отрисовался");
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.up(460));
|
||||
@ -52,6 +62,8 @@ function SupportChat() {
|
||||
const isPreventAutoscroll = useMessageStore(
|
||||
(state) => state.isPreventAutoscroll
|
||||
);
|
||||
const [disableFileButton, setDisableFileButton] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const token = useToken();
|
||||
const ticketId = useParams().ticketId;
|
||||
const ticket = tickets.find((ticket) => ticket.id === ticketId);
|
||||
@ -127,7 +139,6 @@ function SupportChat() {
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[lastMessageId]
|
||||
);
|
||||
|
||||
@ -177,6 +188,45 @@ function SupportChat() {
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
const sendFile = async (file: File) => {
|
||||
if (file === undefined) return true;
|
||||
|
||||
if (ticketId) {
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = new FormData();
|
||||
|
||||
body.append(file.name, file);
|
||||
body.append("ticket", ticketId);
|
||||
await makeRequest({
|
||||
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles",
|
||||
body: body,
|
||||
method: "POST",
|
||||
});
|
||||
} catch (error: any) {
|
||||
const errorMessage = getMessageFromFetchError(error);
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const sendFileHC = async (file: File) => {
|
||||
console.log(file);
|
||||
const check = checkAcceptableMediaType(file);
|
||||
if (check.length > 0) {
|
||||
enqueueSnackbar(check);
|
||||
return;
|
||||
}
|
||||
setDisableFileButton(true);
|
||||
await sendFile(file);
|
||||
setDisableFileButton(false);
|
||||
console.log(disableFileButton);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -262,14 +312,87 @@ function SupportChat() {
|
||||
}}
|
||||
>
|
||||
{ticket &&
|
||||
messages.map((message) => (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={ticket.user === message.user_id}
|
||||
/>
|
||||
))}
|
||||
messages.map((message) => {
|
||||
const isFileVideo = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType)
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileImage = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType)
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileDocument = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType)
|
||||
);
|
||||
}
|
||||
};
|
||||
if (
|
||||
message.files !== null &&
|
||||
message.files.length > 0 &&
|
||||
isFileImage()
|
||||
) {
|
||||
return (
|
||||
<ChatImage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={ticket.user === message.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (
|
||||
message.files !== null &&
|
||||
message.files.length > 0 &&
|
||||
isFileVideo()
|
||||
) {
|
||||
return (
|
||||
<ChatVideo
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={ticket.user === message.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (
|
||||
message.files !== null &&
|
||||
message.files.length > 0 &&
|
||||
isFileDocument()
|
||||
) {
|
||||
return (
|
||||
<ChatDocument
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={ticket.user === message.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={ticket.user === message.user_id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
<FormControl>
|
||||
@ -298,6 +421,27 @@ function SupportChat() {
|
||||
endAdornment={
|
||||
!upMd && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
sx={{ mr: "4px" }}
|
||||
disabled={disableFileButton}
|
||||
onClick={() => {
|
||||
console.log(disableFileButton);
|
||||
if (!disableFileButton) fileInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="fileinput"
|
||||
onChange={({ target }) => {
|
||||
if (target.files?.[0]) {
|
||||
sendFileHC(target.files?.[0]);
|
||||
}
|
||||
}}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={handleSendMessage}
|
||||
sx={{
|
||||
@ -318,6 +462,27 @@ function SupportChat() {
|
||||
</Box>
|
||||
{upMd && (
|
||||
<Box sx={{ alignSelf: "end" }}>
|
||||
<IconButton
|
||||
sx={{ mr: "4px" }}
|
||||
disabled={disableFileButton}
|
||||
onClick={() => {
|
||||
console.log(disableFileButton);
|
||||
if (!disableFileButton) fileInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="fileinput"
|
||||
onChange={({ target }) => {
|
||||
if (target.files?.[0]) {
|
||||
sendFileHC(target.files?.[0]);
|
||||
}
|
||||
}}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
/>
|
||||
<Button
|
||||
variant="pena-contained-dark"
|
||||
onClick={handleSendMessage}
|
||||
|
@ -4,7 +4,6 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import NumberIcon from "@root/components/NumberIcon";
|
||||
import { useDiscountStore } from "@root/stores/discounts";
|
||||
import { useHistoryStore } from "@root/stores/history";
|
||||
import { useTariffStore } from "@root/stores/tariffs";
|
||||
import { addTariffToCart, useUserStore } from "@root/stores/user";
|
||||
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
|
||||
@ -39,7 +38,6 @@ function TariffPage() {
|
||||
const discounts = useDiscountStore((state) => state.discounts);
|
||||
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0;
|
||||
const isUserNko = useUserStore((state) => state.userAccount?.status) === "nko";
|
||||
const historyData = useHistoryStore((state) => state.history);
|
||||
const currentTariffs = useCartTariffs();
|
||||
|
||||
const handleCustomBackNavigation = usePrevLocation(location);
|
||||
@ -70,20 +68,6 @@ function TariffPage() {
|
||||
return false
|
||||
});
|
||||
|
||||
const isCustomTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges.map((p) => p.type).includes("day") === (unit === "time") && !tariff.isDeleted && tariff.isCustom
|
||||
);
|
||||
});
|
||||
|
||||
const tariffsFromHistory = tariffs.filter((tariff) => {
|
||||
if (!historyData) return false;
|
||||
|
||||
const historyTariffIds = historyData.map((historyRecord) => (historyRecord.rawDetails[0] as any).Value[0][0].Value);
|
||||
|
||||
return historyTariffIds.includes(tariff._id);
|
||||
});
|
||||
|
||||
const createTariffElements = (filteredTariffs: Tariff[], addFreeTariff = false) => {
|
||||
console.log(filteredTariffs)
|
||||
const tariffElements = filteredTariffs
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { HistoryRecord } from "@root/api/history";
|
||||
import { HistoryRecord, HistoryRecord2 } from "@root/api/history";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
|
||||
|
||||
type HistoryStore = {
|
||||
history: HistoryRecord[] | null;
|
||||
history: HistoryRecord[] | HistoryRecord2[] | null;
|
||||
};
|
||||
|
||||
const initialState: HistoryStore = {
|
||||
|
@ -1,46 +1,183 @@
|
||||
import { FetchState, Ticket } from "@frontend/kitui"
|
||||
import { create } from "zustand"
|
||||
import { devtools } from "zustand/middleware"
|
||||
import { FetchState, Ticket, TicketMessage } from "@frontend/kitui";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist, createJSONStorage } from "zustand/middleware";
|
||||
import { useUserStore } from "./user";
|
||||
import { produce } from "immer";
|
||||
|
||||
type SessionData = {
|
||||
ticketId: string;
|
||||
sessionId: string;
|
||||
};
|
||||
interface AuthData {
|
||||
sessionData: SessionData | null;
|
||||
isMessageSending: boolean;
|
||||
messages: TicketMessage[];
|
||||
apiPage: number;
|
||||
messagesPerPage: number;
|
||||
lastMessageId: string | undefined;
|
||||
isPreventAutoscroll: boolean;
|
||||
unauthTicketMessageFetchState: FetchState;
|
||||
}
|
||||
|
||||
interface TicketStore {
|
||||
ticketCount: number;
|
||||
tickets: Ticket[];
|
||||
apiPage: number;
|
||||
ticketsPerPage: number;
|
||||
ticketsFetchState: FetchState;
|
||||
ticketCount: number;
|
||||
tickets: Ticket[];
|
||||
apiPage: number;
|
||||
ticketsPerPage: number;
|
||||
ticketsFetchState: FetchState;
|
||||
authData: AuthData;
|
||||
unauthData: AuthData;
|
||||
}
|
||||
|
||||
const initAuthData = {
|
||||
sessionData: null,
|
||||
isMessageSending: false,
|
||||
messages: [],
|
||||
apiPage: 0,
|
||||
messagesPerPage: 10,
|
||||
lastMessageId: undefined,
|
||||
isPreventAutoscroll: false,
|
||||
unauthTicketMessageFetchState: "idle" as FetchState,
|
||||
};
|
||||
|
||||
const initialState: TicketStore = {
|
||||
ticketCount: 0,
|
||||
tickets: [],
|
||||
apiPage: 0,
|
||||
ticketsPerPage: 10,
|
||||
ticketsFetchState: "idle",
|
||||
}
|
||||
ticketCount: 0,
|
||||
tickets: [],
|
||||
apiPage: 0,
|
||||
ticketsPerPage: 10,
|
||||
ticketsFetchState: "idle",
|
||||
authData: initAuthData,
|
||||
unauthData: initAuthData,
|
||||
};
|
||||
|
||||
export const useTicketStore = create<TicketStore>()(
|
||||
devtools(
|
||||
(set, get) => initialState,
|
||||
{
|
||||
name: "Tickets"
|
||||
}
|
||||
)
|
||||
)
|
||||
persist(
|
||||
devtools((set, get) => initialState, {
|
||||
name: "Unauth tickets",
|
||||
}),
|
||||
{
|
||||
version: 0,
|
||||
name: "unauth-ticket",
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const setTicketCount = (ticketCount: number) => useTicketStore.setState({ ticketCount })
|
||||
export const setTicketCount = (ticketCount: number) =>
|
||||
useTicketStore.setState({ ticketCount });
|
||||
|
||||
export const setTicketApiPage = (apiPage: number) => useTicketStore.setState({ apiPage: apiPage })
|
||||
export const setTicketApiPage = (apiPage: number) =>
|
||||
useTicketStore.setState({ apiPage: apiPage });
|
||||
|
||||
export const updateTickets = (receivedTickets: Ticket[]) => {
|
||||
const state = useTicketStore.getState()
|
||||
const ticketIdToTicketMap: { [ticketId: string]: Ticket; } = {};
|
||||
const state = useTicketStore.getState();
|
||||
const ticketIdToTicketMap: { [ticketId: string]: Ticket } = {};
|
||||
|
||||
[...state.tickets, ...receivedTickets].forEach(ticket => ticketIdToTicketMap[ticket.id] = ticket)
|
||||
[...state.tickets, ...receivedTickets].forEach(
|
||||
(ticket) => (ticketIdToTicketMap[ticket.id] = ticket)
|
||||
);
|
||||
|
||||
useTicketStore.setState({ tickets: Object.values(ticketIdToTicketMap) })
|
||||
useTicketStore.setState({ tickets: Object.values(ticketIdToTicketMap) });
|
||||
};
|
||||
|
||||
export const clearTickets = () => useTicketStore.setState({ ...initialState });
|
||||
|
||||
export const setTicketsFetchState = (ticketsFetchState: FetchState) =>
|
||||
useTicketStore.setState({ ticketsFetchState });
|
||||
|
||||
export const setTicketData = (sessionData: SessionData) =>
|
||||
updateTicket((ticket) => {
|
||||
ticket.sessionData = sessionData;
|
||||
});
|
||||
|
||||
export const updateTicket = <T extends AuthData>(
|
||||
recipe: (ticket: AuthData) => void
|
||||
) =>
|
||||
setProducedState(
|
||||
(state) => {
|
||||
//В зависимости от авторизованности вызывается изменение разных объектов
|
||||
if (Boolean(useUserStore.getState().userId)) {
|
||||
recipe(state.authData);
|
||||
} else {
|
||||
recipe(state.unauthData);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "updateTicket",
|
||||
recipe,
|
||||
}
|
||||
);
|
||||
|
||||
function setProducedState<A extends string | { type: unknown }>(
|
||||
recipe: (state: TicketStore) => void,
|
||||
action?: A
|
||||
) {
|
||||
useTicketStore.setState((state) => produce(state, recipe), false, action);
|
||||
}
|
||||
|
||||
export const clearTickets = () => useTicketStore.setState({ ...initialState })
|
||||
function filterMessageUncompleteness(messages: TicketMessage[]) {
|
||||
return messages.filter(
|
||||
(message) =>
|
||||
"id" in message &&
|
||||
"ticket_id" in message &&
|
||||
"user_id" in message &&
|
||||
"session_id" in message &&
|
||||
"message" in message &&
|
||||
"files" in message &&
|
||||
"shown" in message &&
|
||||
"request_screenshot" in message &&
|
||||
"created_at" in message &&
|
||||
((message.files !== null && message.files.length > 0) ||
|
||||
message.message.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
export const setTicketsFetchState = (ticketsFetchState: FetchState) => useTicketStore.setState({ ticketsFetchState })
|
||||
function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) {
|
||||
const date1 = new Date(ticket1.created_at).getTime();
|
||||
const date2 = new Date(ticket2.created_at).getTime();
|
||||
return date1 - date2;
|
||||
}
|
||||
|
||||
export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) =>
|
||||
updateTicket((ticket) => {
|
||||
const filtered = filterMessageUncompleteness(receivedMessages);
|
||||
if (filtered.length === 0) return;
|
||||
|
||||
const messageIdToMessageMap: { [messageId: string]: TicketMessage } = {};
|
||||
|
||||
[...ticket.messages, ...filtered].forEach(
|
||||
(message) => (messageIdToMessageMap[message.id] = message)
|
||||
);
|
||||
|
||||
const sortedMessages = Object.values(messageIdToMessageMap).sort(
|
||||
sortMessagesByTime
|
||||
);
|
||||
|
||||
ticket.messages = sortedMessages;
|
||||
ticket.lastMessageId = sortedMessages.at(-1)?.id;
|
||||
});
|
||||
|
||||
export const setUnauthTicketMessageFetchState = (
|
||||
unauthTicketMessageFetchState: FetchState
|
||||
) =>
|
||||
updateTicket((ticket) => {
|
||||
ticket.unauthTicketMessageFetchState = unauthTicketMessageFetchState;
|
||||
});
|
||||
|
||||
export const setUnauthIsPreventAutoscroll = (isPreventAutoscroll: boolean) =>
|
||||
updateTicket((ticket) => {
|
||||
ticket.isPreventAutoscroll = isPreventAutoscroll;
|
||||
});
|
||||
|
||||
export const incrementUnauthMessage = () =>
|
||||
updateTicket((ticket) => {
|
||||
ticket.apiPage++;
|
||||
});
|
||||
|
||||
export const setIsMessageSending = (
|
||||
isMessageSending: AuthData["isMessageSending"]
|
||||
) => {
|
||||
updateTicket((ticket) => {
|
||||
ticket.isMessageSending = isMessageSending;
|
||||
});
|
||||
};
|
||||
|
@ -1,79 +0,0 @@
|
||||
import { FetchState, TicketMessage } from "@frontend/kitui"
|
||||
import { create } from "zustand"
|
||||
import { createJSONStorage, devtools, persist } from "zustand/middleware"
|
||||
|
||||
|
||||
interface UnauthTicketStore {
|
||||
sessionData: {
|
||||
ticketId: string;
|
||||
sessionId: string;
|
||||
} | null;
|
||||
isMessageSending: boolean;
|
||||
messages: TicketMessage[];
|
||||
apiPage: number;
|
||||
messagesPerPage: number;
|
||||
lastMessageId: string | undefined;
|
||||
isPreventAutoscroll: boolean;
|
||||
unauthTicketMessageFetchState: FetchState;
|
||||
}
|
||||
|
||||
export const useUnauthTicketStore = create<UnauthTicketStore>()(
|
||||
persist(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
sessionData: null,
|
||||
isMessageSending: false,
|
||||
messages: [],
|
||||
apiPage: 0,
|
||||
messagesPerPage: 10,
|
||||
lastMessageId: undefined,
|
||||
isPreventAutoscroll: false,
|
||||
unauthTicketMessageFetchState: "idle",
|
||||
}),
|
||||
{
|
||||
name: "Unauth tickets"
|
||||
}
|
||||
),
|
||||
{
|
||||
version: 0,
|
||||
name: "unauth-ticket",
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: state => ({
|
||||
sessionData: state.sessionData,
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export const setUnauthSessionData = (sessionData: UnauthTicketStore["sessionData"]) => useUnauthTicketStore.setState({ sessionData })
|
||||
export const setIsMessageSending = (isMessageSending: UnauthTicketStore["isMessageSending"]) => useUnauthTicketStore.setState({ isMessageSending })
|
||||
|
||||
export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) => {
|
||||
const state = useUnauthTicketStore.getState()
|
||||
const messageIdToMessageMap: { [messageId: string]: TicketMessage; } = {};
|
||||
|
||||
[...state.messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message)
|
||||
|
||||
const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime)
|
||||
|
||||
useUnauthTicketStore.setState({
|
||||
messages: sortedMessages,
|
||||
lastMessageId: sortedMessages.at(-1)?.id,
|
||||
})
|
||||
}
|
||||
|
||||
export const incrementUnauthMessageApiPage = () => {
|
||||
const state = useUnauthTicketStore.getState()
|
||||
|
||||
useUnauthTicketStore.setState({ apiPage: state.apiPage + 1 })
|
||||
}
|
||||
|
||||
export const setUnauthIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useUnauthTicketStore.setState({ isPreventAutoscroll })
|
||||
|
||||
export const setUnauthTicketMessageFetchState = (unauthTicketMessageFetchState: FetchState) => useUnauthTicketStore.setState({ unauthTicketMessageFetchState })
|
||||
|
||||
function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) {
|
||||
const date1 = new Date(ticket1.created_at).getTime()
|
||||
const date2 = new Date(ticket2.created_at).getTime()
|
||||
return date1 - date2
|
||||
}
|
37
src/utils/checkAcceptableMediaType.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export const MAX_FILE_SIZE = 10485760;
|
||||
const MAX_PHOTO_SIZE = 5242880;
|
||||
const MAX_VIDEO_SIZE = 52428800;
|
||||
|
||||
export const ACCEPT_SEND_MEDIA_TYPES_MAP = {
|
||||
picture: ["jpg", "png"],
|
||||
video: ["mp4"],
|
||||
document: ["doc", "docx", "pdf", "txt", "xlsx", "csv"],
|
||||
} as const;
|
||||
|
||||
const TOO_LARGE_TEXT = "Файл слишком большой";
|
||||
|
||||
export const checkAcceptableMediaType = (file: File) => {
|
||||
if (file === null) return "";
|
||||
|
||||
const segments = file?.name.split(".");
|
||||
const extension = segments[segments.length - 1];
|
||||
const type = extension.toLowerCase();
|
||||
|
||||
console.log(type);
|
||||
switch (type) {
|
||||
case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find((name) => name === type):
|
||||
if (file.size > MAX_FILE_SIZE) return TOO_LARGE_TEXT;
|
||||
return "";
|
||||
|
||||
case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find((name) => name === type):
|
||||
if (file.size > MAX_PHOTO_SIZE) return TOO_LARGE_TEXT;
|
||||
return "";
|
||||
|
||||
case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find((name) => name === type):
|
||||
if (file.size > MAX_VIDEO_SIZE) return TOO_LARGE_TEXT;
|
||||
return "";
|
||||
|
||||
default:
|
||||
return "Не удалось отправить файл. Недопустимый тип";
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { GetHistoryResponse, getHistory } from "@root/api/history";
|
||||
import { setHistory, useHistoryStore } from "@root/stores/history";
|
||||
import { setHistory } from "@root/stores/history";
|
||||
|
||||
export const useHistoryData = () => {
|
||||
|
||||
@ -8,7 +8,6 @@ export const useHistoryData = () => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [response, errorMsg] = await getHistory();
|
||||
|
||||
if (errorMsg) {
|
||||
console.error("Произошла ошибка при вызове getHistory:", errorMsg);
|
||||
}
|
||||
|
@ -1,26 +1,19 @@
|
||||
import {useEffect, useState} from "react"
|
||||
import { useTariffStore } from "@root/stores/tariffs";
|
||||
import {getRecentlyPurchasedTariffs} from "@root/api/recentlyPurchasedTariffs"
|
||||
import { getTariffById } from "@root/api/tariff"
|
||||
import {useHistoryStore} from "@stores/history"
|
||||
|
||||
export const useRecentlyPurchasedTariffs = () => {
|
||||
const [recentlyPurchased, setRecentlyPurchased] = useState<any>([])
|
||||
const tariffs = useTariffStore((state) => state.tariffs);
|
||||
const historyData = useHistoryStore(state => state.history);
|
||||
useEffect(() => {
|
||||
console.log("юзэффект начинает работаььб")
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [response, errorMsg] = await getRecentlyPurchasedTariffs();
|
||||
console.log("responce" , response)
|
||||
if (errorMsg) {
|
||||
console.error("Произошла ошибка при вызове getRecentlyPurchasedTariffs:", errorMsg);
|
||||
}
|
||||
if (response) {
|
||||
const recentlyTariffs = response.slice(0, 10).map((obj: { id: string })=>obj.id)
|
||||
console.log("responce22222222222222222" , recentlyTariffs)
|
||||
console.log("tariffstariffstariffstariffstariffstariffs" , tariffs)
|
||||
setRecentlyPurchased(tariffs.filter((tariffs)=>{
|
||||
return recentlyTariffs.includes(tariffs._id)}));
|
||||
}
|
||||
@ -31,6 +24,6 @@ console.log("responce" , response)
|
||||
|
||||
fetchData();
|
||||
}, [tariffs]);
|
||||
console.log(recentlyPurchased)
|
||||
|
||||
return {recentlyPurchased}
|
||||
}
|
||||
}
|
||||
|
11
yarn.lock
@ -4002,7 +4002,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001591:
|
||||
version "1.0.30001593"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001593.tgz#7cda1d9e5b0cad6ebab4133b1f239d4ea44fe659"
|
||||
integrity sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ==
|
||||
|
||||
canvas@^2.11.2:
|
||||
version "2.11.2"
|
||||
resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.2.tgz#553d87b1e0228c7ac0fc72887c3adbac4abbd860"
|
||||
@ -5756,7 +5755,6 @@ fast-glob@^3.2.9, fast-glob@^3.3.0:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
@ -6336,7 +6334,6 @@ hasown@^2.0.0, hasown@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa"
|
||||
integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
he@^1.2.0:
|
||||
@ -7015,7 +7012,6 @@ jake@^10.8.5:
|
||||
version "10.8.7"
|
||||
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f"
|
||||
integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==
|
||||
dependencies:
|
||||
async "^3.2.3"
|
||||
chalk "^4.0.2"
|
||||
filelist "^1.0.4"
|
||||
@ -8259,7 +8255,6 @@ lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||
|
||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||
@ -8421,7 +8416,6 @@ minimatch@^5.0.1:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
|
||||
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8:
|
||||
@ -8754,7 +8748,6 @@ open@^8.0.9, open@^8.4.0:
|
||||
version "8.4.2"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
|
||||
integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
|
||||
dependencies:
|
||||
define-lazy-prop "^2.0.0"
|
||||
is-docker "^2.1.1"
|
||||
is-wsl "^2.2.0"
|
||||
@ -9517,7 +9510,6 @@ postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-select
|
||||
version "6.0.15"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535"
|
||||
integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
@ -10020,7 +10012,6 @@ regenerate-unicode-properties@^10.1.0:
|
||||
version "10.1.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480"
|
||||
integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==
|
||||
dependencies:
|
||||
regenerate "^1.4.2"
|
||||
|
||||
regenerate@^1.4.2:
|
||||
@ -10154,7 +10145,6 @@ resolve.exports@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999"
|
||||
integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==
|
||||
|
||||
resolve.exports@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800"
|
||||
@ -10397,7 +10387,6 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
|
||||
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
serve-index@^1.9.1:
|
||||
|