Merge branch 'dev' into 'main'
Dev See merge request frontend/marketplace!65
This commit is contained in:
commit
91fd8a675a
@ -14,7 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@frontend/kitui": "1.0.53",
|
"@frontend/kitui": "1.0.54",
|
||||||
"@mui/icons-material": "^5.10.14",
|
"@mui/icons-material": "^5.10.14",
|
||||||
"@mui/material": "^5.10.14",
|
"@mui/material": "^5.10.14",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
@ -29,6 +29,7 @@
|
|||||||
"pdfjs-dist": "3.6.172",
|
"pdfjs-dist": "3.6.172",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-error-boundary": "^4.0.11",
|
||||||
"react-pdf": "^7.1.2",
|
"react-pdf": "^7.1.2",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"react-slick": "^0.29.0",
|
"react-slick": "^0.29.0",
|
||||||
|
@ -35,7 +35,7 @@ export async function verification(
|
|||||||
|
|
||||||
export async function sendDocuments(
|
export async function sendDocuments(
|
||||||
documents: SendDocumentsArgs
|
documents: SendDocumentsArgs
|
||||||
): Promise<[Verification | null, string?]> {
|
): Promise<[Verification | "OK" | null, string?]> {
|
||||||
try {
|
try {
|
||||||
const sendDocumentsResponse = await makeRequest<FormData, Verification>({
|
const sendDocumentsResponse = await makeRequest<FormData, Verification>({
|
||||||
url: apiUrl + "/verification",
|
url: apiUrl + "/verification",
|
||||||
@ -55,7 +55,7 @@ export async function sendDocuments(
|
|||||||
|
|
||||||
export async function updateDocuments(
|
export async function updateDocuments(
|
||||||
documents: UpdateDocumentsArgs
|
documents: UpdateDocumentsArgs
|
||||||
): Promise<[Verification | null, string?]> {
|
): Promise<[Verification | "OK" | null, string? ]> {
|
||||||
try {
|
try {
|
||||||
const updateDocumentsResponse = await makeRequest<FormData, Verification>({
|
const updateDocumentsResponse = await makeRequest<FormData, Verification>({
|
||||||
url: apiUrl + "/verification/file",
|
url: apiUrl + "/verification/file",
|
||||||
|
@ -30,8 +30,11 @@ import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
|
|||||||
import { payCart } from "@root/api/cart";
|
import { payCart } from "@root/api/cart";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||||
|
|
||||||
export default function Drawers() {
|
function Drawers() {
|
||||||
const [openNotificationsModal, setOpenNotificationsModal] =
|
const [openNotificationsModal, setOpenNotificationsModal] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
@ -57,11 +60,14 @@ export default function Drawers() {
|
|||||||
const [payCartResponse, payCartError] = await payCart();
|
const [payCartResponse, payCartError] = await payCart();
|
||||||
|
|
||||||
if (payCartError) {
|
if (payCartError) {
|
||||||
|
if (payCartError.includes("insufficient funds: ")) {
|
||||||
const notEnoughMoneyAmount = parseInt(
|
const notEnoughMoneyAmount = parseInt(
|
||||||
payCartError.replace("insufficient funds: ", "")
|
payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
|
||||||
);
|
);
|
||||||
|
|
||||||
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
|
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
return enqueueSnackbar(payCartError);
|
return enqueueSnackbar(payCartError);
|
||||||
@ -72,6 +78,7 @@ export default function Drawers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
closeCartDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReplenishWallet() {
|
function handleReplenishWallet() {
|
||||||
@ -303,8 +310,6 @@ export default function Drawers() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Button
|
<Button
|
||||||
variant="pena-contained-dark"
|
variant="pena-contained-dark"
|
||||||
component={Link}
|
|
||||||
to="/cart"
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
notEnoughMoneyAmount === 0
|
notEnoughMoneyAmount === 0
|
||||||
? !loading && handlePayClick()
|
? !loading && handlePayClick()
|
||||||
@ -322,3 +327,15 @@ export default function Drawers() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(Drawers, {
|
||||||
|
fallback: (
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<ErrorOutlineIcon color="error" />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
onError: handleComponentError,
|
||||||
|
})
|
||||||
|
@ -15,11 +15,26 @@ export default function Menu() {
|
|||||||
|
|
||||||
const color = location.pathname === "/" ? "white" : "black";
|
const color = location.pathname === "/" ? "white" : "black";
|
||||||
|
|
||||||
const arrayMenu: MenuItem[] = [
|
const arrayMenu: MenuItem[] = location.pathname === "/" ? [
|
||||||
{ name: "Наши продукты", url: "/faq" },
|
{ name: "Наши продукты", url: "/faq" },
|
||||||
{ name: "Наши услуги", url: "/cart" }
|
{ name: "Наши услуги", url: "/cart" }
|
||||||
|
] : [
|
||||||
|
{
|
||||||
|
name: "Тарифы",
|
||||||
|
url: "/tariffs",
|
||||||
|
subMenu: [
|
||||||
|
{ name: "Тарифы на время", url: "/tariffs/time" },
|
||||||
|
{ name: "Тарифы на объём", url: "/tariffs/volume" },
|
||||||
|
{ name: "Кастомный тариф", url: "/tariffconstructor" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: "Вопросы и ответы", url: "/faq" },
|
||||||
|
{ name: "Корзина", url: "/cart" },
|
||||||
|
{ name: "Поддержка", url: "/support" },
|
||||||
|
{ name: "История", url: "/history" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -38,11 +38,14 @@ export default function TotalPrice({
|
|||||||
const [payCartResponse, payCartError] = await payCart();
|
const [payCartResponse, payCartError] = await payCart();
|
||||||
|
|
||||||
if (payCartError) {
|
if (payCartError) {
|
||||||
|
if (payCartError.includes("insufficient funds: ")) {
|
||||||
const notEnoughMoneyAmount = parseInt(
|
const notEnoughMoneyAmount = parseInt(
|
||||||
payCartError.replace("insufficient funds: ", "")
|
payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
|
||||||
);
|
);
|
||||||
|
|
||||||
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
|
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
return enqueueSnackbar(payCartError);
|
return enqueueSnackbar(payCartError);
|
||||||
|
@ -20,10 +20,10 @@ export default function CustomIcon() {
|
|||||||
|
|
||||||
>
|
>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" stroke="inherit" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" stroke="inherit" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="3.2002" y="0.75" width="17.8" height="22.25" rx="4" stroke-width="1.5"/>
|
<rect x="3.2002" y="0.75" width="17.8" height="22.25" rx="4" strokeWidth="1.5"/>
|
||||||
<path d="M7.65039 6.3125H16.5504" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M7.65039 6.3125H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
|
||||||
<path d="M7.65039 11.875H16.5504" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M7.65039 11.875H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
|
||||||
<path d="M7.65039 17.4375H12.1004" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M7.65039 17.4375H12.1004" strokeWidth="1.5" strokeLinecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -29,6 +29,7 @@ import { theme } from "./utils/theme";
|
|||||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString();
|
pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString();
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
console.log("render app")
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const userId = useUserStore((state) => state.userId);
|
const userId = useUserStore((state) => state.userId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -101,7 +102,7 @@ const App = () => {
|
|||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
// <React.StrictMode>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
@ -109,7 +110,7 @@ root.render(
|
|||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
// </React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { Box, Button, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Button, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import InputTextfield from "@components/InputTextfield";
|
import InputTextfield from "@components/InputTextfield";
|
||||||
import PasswordInput from "@components/passwordInput";
|
import PasswordInput from "@components/passwordInput";
|
||||||
|
import UserFields from "./UserFields";
|
||||||
import SectionWrapper from "@components/SectionWrapper";
|
import SectionWrapper from "@components/SectionWrapper";
|
||||||
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user";
|
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user";
|
||||||
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon";
|
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon";
|
||||||
@ -13,8 +14,10 @@ import { getMessageFromFetchError } from "@frontend/kitui";
|
|||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { VerificationStatus } from "@root/model/account";
|
import { VerificationStatus } from "@root/model/account";
|
||||||
import { verify } from "./helper";
|
import { verify } from "./helper";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
|
||||||
export default function AccountSettings() {
|
function AccountSettings() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||||
@ -37,12 +40,6 @@ export default function AccountSettings() {
|
|||||||
bold: true,
|
bold: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const verificationStatusData: Record<VerificationStatus, { text: string; color: string }> = {
|
|
||||||
verificated: { text: "Верификация пройдена", color: "#0D9F00" },
|
|
||||||
waiting: { text: "В ожидании верификации", color: "#F18956" },
|
|
||||||
notVerificated: { text: "Не верифицирован", color: "#E02C2C" },
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleSendDataClick() {
|
function handleSendDataClick() {
|
||||||
sendUserData()
|
sendUserData()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -54,32 +51,6 @@ export default function AccountSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function VerificationIndicator({
|
|
||||||
verificationStatus,
|
|
||||||
sx,
|
|
||||||
}: {
|
|
||||||
verificationStatus: VerificationStatus;
|
|
||||||
sx?: SxProps<Theme>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
py: "14px",
|
|
||||||
px: "8.5px",
|
|
||||||
borderWidth: "1px",
|
|
||||||
borderStyle: "solid",
|
|
||||||
color: verificationStatusData[verificationStatus].color,
|
|
||||||
borderColor: verificationStatusData[verificationStatus].color,
|
|
||||||
borderRadius: "8px",
|
|
||||||
textAlign: "center",
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography lineHeight="100%">{verificationStatusData[verificationStatus].text}</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWrapper
|
<SectionWrapper
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
@ -114,103 +85,7 @@ export default function AccountSettings() {
|
|||||||
flexDirection: upMd ? "row" : "column",
|
flexDirection: upMd ? "row" : "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<UserFields/>
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridAutoFlow: upSm ? "column" : "row",
|
|
||||||
gridTemplateRows: "repeat(4, auto)",
|
|
||||||
gridAutoColumns: "1fr",
|
|
||||||
rowGap: "15px",
|
|
||||||
columnGap: "31px",
|
|
||||||
flexGrow: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InputTextfield
|
|
||||||
TextfieldProps={{
|
|
||||||
placeholder: "Имя",
|
|
||||||
value: fields.firstname.value || "",
|
|
||||||
helperText: fields.firstname.touched && fields.firstname.error,
|
|
||||||
error: fields.firstname.touched && Boolean(fields.firstname.error),
|
|
||||||
}}
|
|
||||||
onChange={(e) => setSettingsField("firstname", e.target.value)}
|
|
||||||
id="firstname"
|
|
||||||
label="Имя"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
<InputTextfield
|
|
||||||
TextfieldProps={{
|
|
||||||
placeholder: "Фамилия",
|
|
||||||
value: fields.secondname.value || "",
|
|
||||||
helperText: fields.secondname.touched && fields.secondname.error,
|
|
||||||
error: fields.secondname.touched && Boolean(fields.secondname.error),
|
|
||||||
}}
|
|
||||||
onChange={(e) => setSettingsField("secondname", e.target.value)}
|
|
||||||
id="secondname"
|
|
||||||
label="Фамилия"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
<InputTextfield
|
|
||||||
TextfieldProps={{
|
|
||||||
placeholder: "Отчество",
|
|
||||||
value: fields.middlename.value || "",
|
|
||||||
helperText: fields.middlename.touched && fields.middlename.error,
|
|
||||||
error: fields.middlename.touched && Boolean(fields.middlename.error),
|
|
||||||
}}
|
|
||||||
onChange={(e) => setSettingsField("middlename", e.target.value)}
|
|
||||||
id="middlename"
|
|
||||||
label="Отчество"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
<InputTextfield
|
|
||||||
TextfieldProps={{
|
|
||||||
placeholder: "ООО Фирма",
|
|
||||||
value: fields.orgname.value || "",
|
|
||||||
helperText: fields.orgname.touched && fields.orgname.error,
|
|
||||||
error: fields.orgname.touched && Boolean(fields.orgname.error),
|
|
||||||
}}
|
|
||||||
onChange={(e) => setSettingsField("orgname", e.target.value)}
|
|
||||||
id="orgname"
|
|
||||||
label="Название компании"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
<InputTextfield
|
|
||||||
TextfieldProps={{
|
|
||||||
placeholder: "username@penahaub.com",
|
|
||||||
value: fields.email.value || "",
|
|
||||||
helperText: fields.email.touched && fields.email.error,
|
|
||||||
error: fields.email.touched && Boolean(fields.email.error),
|
|
||||||
}}
|
|
||||||
onChange={(e) => setSettingsField("email", e.target.value)}
|
|
||||||
id="email"
|
|
||||||
label="E-mail"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
<InputTextfield
|
|
||||||
TextfieldProps={{
|
|
||||||
placeholder: "+7 900 000 00 00",
|
|
||||||
value: fields.phoneNumber.value || "",
|
|
||||||
helperText: fields.phoneNumber.touched && fields.phoneNumber.error,
|
|
||||||
error: fields.phoneNumber.touched && Boolean(fields.phoneNumber.error),
|
|
||||||
}}
|
|
||||||
onChange={(e) => setSettingsField("phoneNumber", e.target.value)}
|
|
||||||
id="phoneNumber"
|
|
||||||
label="Телефон"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
TextfieldProps={{
|
|
||||||
placeholder: "Не менее 8 символов",
|
|
||||||
value: fields.password.value || "",
|
|
||||||
helperText: fields.password.touched && fields.password.error,
|
|
||||||
error: fields.password.touched && Boolean(fields.password.error),
|
|
||||||
autoComplete: "new-password",
|
|
||||||
}}
|
|
||||||
onChange={(e) => setSettingsField("password", e.target.value)}
|
|
||||||
id="password"
|
|
||||||
label="Пароль"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: "246px",
|
maxWidth: "246px",
|
||||||
@ -266,3 +141,40 @@ export default function AccountSettings() {
|
|||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(AccountSettings, {
|
||||||
|
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении настроек аккаунта</Typography>,
|
||||||
|
onError: handleComponentError,
|
||||||
|
})
|
||||||
|
|
||||||
|
const verificationStatusData: Record<VerificationStatus, { text: string; color: string; }> = {
|
||||||
|
verificated: { text: "Верификация пройдена", color: "#0D9F00" },
|
||||||
|
waiting: { text: "В ожидании верификации", color: "#F18956" },
|
||||||
|
notVerificated: { text: "Не верифицирован", color: "#E02C2C" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function VerificationIndicator({
|
||||||
|
verificationStatus,
|
||||||
|
sx,
|
||||||
|
}: {
|
||||||
|
verificationStatus: VerificationStatus;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: "14px",
|
||||||
|
px: "8.5px",
|
||||||
|
borderWidth: "1px",
|
||||||
|
borderStyle: "solid",
|
||||||
|
color: verificationStatusData[verificationStatus].color,
|
||||||
|
borderColor: verificationStatusData[verificationStatus].color,
|
||||||
|
borderRadius: "8px",
|
||||||
|
textAlign: "center",
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography lineHeight="100%">{verificationStatusData[verificationStatus].text}</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -4,7 +4,14 @@ import JuridicalDocumentsDialog from "./JuridicalDocumentsDialog";
|
|||||||
|
|
||||||
|
|
||||||
export default function DocumentsDialog() {
|
export default function DocumentsDialog() {
|
||||||
const type = useUserStore(state => state.dialogType);
|
switch(useUserStore(state => state.dialogType)) {
|
||||||
|
case 'juridical':
|
||||||
|
return <JuridicalDocumentsDialog />
|
||||||
|
|
||||||
return type === "juridical" ? <JuridicalDocumentsDialog /> : <NkoDocumentsDialog />
|
case "nko":
|
||||||
|
return <NkoDocumentsDialog />
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ import { readFile } from "@root/utils/readFile";
|
|||||||
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys";
|
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys";
|
||||||
import { verify } from "../helper";
|
import { verify } from "../helper";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { theme } from "@root/utils/theme";
|
||||||
|
|
||||||
const dialogContainerStyle = {
|
const dialogContainerStyle = {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@ -26,15 +27,15 @@ const dialogContainerStyle = {
|
|||||||
export default function JuridicalDocumentsDialog() {
|
export default function JuridicalDocumentsDialog() {
|
||||||
const isOpen = useUserStore((state) => state.isDocumentsDialogOpen);
|
const isOpen = useUserStore((state) => state.isDocumentsDialogOpen);
|
||||||
const verificationStatus = useUserStore((state) => state.verificationStatus);
|
const verificationStatus = useUserStore((state) => state.verificationStatus);
|
||||||
const documents = useUserStore((state) => state.documents);
|
const documents = useUserStore((state) => state.documents);//загруженные юзером файлы
|
||||||
const documentsUrl = useUserStore((state) => state.documentsUrl);
|
const documentsUrl = useUserStore((state) => state.documentsUrl);//ссылки с бекенда
|
||||||
const userId = useUserStore((state) => state.userId) ?? "";
|
const userId = useUserStore((state) => state.userId) ?? "";
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const sendUploadedDocuments = async () => {
|
const sendUploadedDocuments = async () => {
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
if (documents["ИНН"].file && documents["Устав"].file) {
|
if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) {
|
||||||
|
closeDocumentsDialog();
|
||||||
|
//Пользователь заполнил все поля и на беке пусто
|
||||||
const inn = await readFile(documents["ИНН"].file, "binary");
|
const inn = await readFile(documents["ИНН"].file, "binary");
|
||||||
const rule = await readFile(documents["Устав"].file, "binary");
|
const rule = await readFile(documents["Устав"].file, "binary");
|
||||||
|
|
||||||
@ -46,10 +47,19 @@ export default function JuridicalDocumentsDialog() {
|
|||||||
|
|
||||||
if (sendDocumentsError) {
|
if (sendDocumentsError) {
|
||||||
enqueueSnackbar(sendDocumentsError);
|
enqueueSnackbar(sendDocumentsError);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
if (_ === "OK") {
|
||||||
|
enqueueSnackbar("Информация доставлена")
|
||||||
|
}
|
||||||
|
|
||||||
|
setDocument("ИНН", null);
|
||||||
|
setDocument("Устав", null);
|
||||||
|
|
||||||
|
await verify(userId);
|
||||||
|
} else { //Пользователь заполнил не все, или на беке что-то есть
|
||||||
|
if ((documents["ИНН"].file || documents["Устав"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано
|
||||||
|
closeDocumentsDialog();
|
||||||
const inn = documents["ИНН"].file
|
const inn = documents["ИНН"].file
|
||||||
? await readFile(documents["ИНН"].file, "binary")
|
? await readFile(documents["ИНН"].file, "binary")
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -70,18 +80,31 @@ export default function JuridicalDocumentsDialog() {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_ === "OK") {
|
||||||
|
enqueueSnackbar("Информация доставлена")
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
closeDocumentsDialog();
|
|
||||||
|
|
||||||
setDocument("ИНН", null);
|
setDocument("ИНН", null);
|
||||||
setDocument("Устав", null);
|
setDocument("Устав", null);
|
||||||
|
|
||||||
await verify(userId);
|
await verify(userId);
|
||||||
if (!isLoading) closeDocumentsDialog();
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disbutt = () => {
|
||||||
|
if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) { //post
|
||||||
|
//все поля заполнены и на беке пусто
|
||||||
|
return false
|
||||||
|
} else {//patch
|
||||||
|
if (documents["ИНН"].file || documents["Устав"].file) {
|
||||||
|
//минимум одно поле заполнено
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const documentElements =
|
const documentElements =
|
||||||
verificationStatus === VerificationStatus.VERIFICATED ? (
|
verificationStatus === VerificationStatus.VERIFICATED ? (
|
||||||
<>
|
<>
|
||||||
@ -163,6 +186,18 @@ export default function JuridicalDocumentsDialog() {
|
|||||||
>
|
>
|
||||||
для верификации юридических лиц в формате PDF
|
для верификации юридических лиц в формате PDF
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"]) && <Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "100%",
|
||||||
|
mt: "12px",
|
||||||
|
color: theme.palette.purple.main
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Пожалуйста, заполните все поля!
|
||||||
|
</Typography>}
|
||||||
|
|
||||||
<Box sx={dialogContainerStyle}>
|
<Box sx={dialogContainerStyle}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -182,7 +217,7 @@ export default function JuridicalDocumentsDialog() {
|
|||||||
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
|
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
|
||||||
onClick={sendUploadedDocuments}
|
onClick={sendUploadedDocuments}
|
||||||
variant="pena-contained-dark"
|
variant="pena-contained-dark"
|
||||||
disabled={isLoading}
|
disabled={disbutt()}
|
||||||
>
|
>
|
||||||
Отправить
|
Отправить
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -14,6 +14,7 @@ import { sendDocuments, updateDocuments } from "@root/api/verification";
|
|||||||
import { readFile } from "@root/utils/readFile";
|
import { readFile } from "@root/utils/readFile";
|
||||||
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys";
|
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { theme } from "@root/utils/theme";
|
||||||
|
|
||||||
const dialogContainerStyle = {
|
const dialogContainerStyle = {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@ -29,15 +30,16 @@ export default function NkoDocumentsDialog() {
|
|||||||
const documents = useUserStore((state) => state.documents);
|
const documents = useUserStore((state) => state.documents);
|
||||||
const documentsUrl = useUserStore((state) => state.documentsUrl);
|
const documentsUrl = useUserStore((state) => state.documentsUrl);
|
||||||
const userId = useUserStore((state) => state.userId) ?? "";
|
const userId = useUserStore((state) => state.userId) ?? "";
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const sendUploadedDocuments = async () => {
|
const sendUploadedDocuments = async () => {
|
||||||
setIsLoading(true);
|
|
||||||
if (
|
if (
|
||||||
documents["ИНН"].file &&
|
documents["ИНН"].file &&
|
||||||
documents["Устав"].file &&
|
documents["Устав"].file &&
|
||||||
documents["Свидетельство о регистрации НКО"].file
|
documents["Свидетельство о регистрации НКО"].file
|
||||||
|
&& !documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]
|
||||||
) {
|
) {
|
||||||
|
closeDocumentsDialog();
|
||||||
|
//Пользователь заполнил все поля и на беке пусто
|
||||||
const inn = await readFile(documents["ИНН"].file, "binary");
|
const inn = await readFile(documents["ИНН"].file, "binary");
|
||||||
const rule = await readFile(documents["Устав"].file, "binary");
|
const rule = await readFile(documents["Устав"].file, "binary");
|
||||||
const certificate = await readFile(
|
const certificate = await readFile(
|
||||||
@ -57,7 +59,18 @@ export default function NkoDocumentsDialog() {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
if (_ === "OK") {
|
||||||
|
enqueueSnackbar("Информация доставлена")
|
||||||
|
}
|
||||||
|
|
||||||
|
setDocument("ИНН", null);
|
||||||
|
setDocument("Устав", null);
|
||||||
|
setDocument("Свидетельство о регистрации НКО", null);
|
||||||
|
|
||||||
|
await verify(userId);
|
||||||
|
} else { //Пользователь заполнил не все, или на беке что-то есть
|
||||||
|
if ((documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"] || documentsUrl["Свидетельство о регистрации НКО"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано
|
||||||
|
closeDocumentsDialog();
|
||||||
const inn = documents["ИНН"].file
|
const inn = documents["ИНН"].file
|
||||||
? await readFile(documents["ИНН"].file, "binary")
|
? await readFile(documents["ИНН"].file, "binary")
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -85,18 +98,33 @@ export default function NkoDocumentsDialog() {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_ === "OK") {
|
||||||
|
enqueueSnackbar("Информация доставлена")
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
closeDocumentsDialog();
|
|
||||||
|
|
||||||
setDocument("ИНН", null);
|
setDocument("ИНН", null);
|
||||||
setDocument("Устав", null);
|
setDocument("Устав", null);
|
||||||
setDocument("Свидетельство о регистрации НКО", null);
|
setDocument("Свидетельство о регистрации НКО", null);
|
||||||
|
|
||||||
await verify(userId);
|
await verify(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disbutt = () => {
|
||||||
|
if (documents["ИНН"].file && documents["Устав"].file && documents["Свидетельство о регистрации НКО"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) { //post
|
||||||
|
//все поля заполнены и на беке пусто
|
||||||
|
return false
|
||||||
|
} else {//patch
|
||||||
|
if (documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file ) {
|
||||||
|
//минимум одно поле заполнено
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const documentElements =
|
const documentElements =
|
||||||
verificationStatus === VerificationStatus.VERIFICATED ? (
|
verificationStatus === VerificationStatus.VERIFICATED ? (
|
||||||
<>
|
<>
|
||||||
@ -194,6 +222,17 @@ export default function NkoDocumentsDialog() {
|
|||||||
>
|
>
|
||||||
для верификации НКО в формате PDF
|
для верификации НКО в формате PDF
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) && <Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "100%",
|
||||||
|
mt: "12px",
|
||||||
|
color: theme.palette.purple.main
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Пожалуйста, заполните все поля!
|
||||||
|
</Typography>}
|
||||||
<Box sx={dialogContainerStyle}>
|
<Box sx={dialogContainerStyle}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -213,7 +252,7 @@ export default function NkoDocumentsDialog() {
|
|||||||
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
|
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
|
||||||
onClick={sendUploadedDocuments}
|
onClick={sendUploadedDocuments}
|
||||||
variant="pena-contained-dark"
|
variant="pena-contained-dark"
|
||||||
disabled={isLoading}
|
disabled={disbutt()}
|
||||||
>
|
>
|
||||||
Отправить
|
Отправить
|
||||||
</Button>
|
</Button>
|
||||||
|
121
src/pages/AccountSettings/UserFields.tsx
Normal file
121
src/pages/AccountSettings/UserFields.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { Box, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
import InputTextfield from "@components/InputTextfield";
|
||||||
|
import PasswordInput from "@components/passwordInput";
|
||||||
|
import { setSettingsField, useUserStore } from "@root/stores/user";
|
||||||
|
|
||||||
|
|
||||||
|
export default function UserFields () {
|
||||||
|
const theme = useTheme();
|
||||||
|
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||||
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
|
||||||
|
const fields = useUserStore((state) => state.settingsFields);
|
||||||
|
|
||||||
|
console.log("fields")
|
||||||
|
|
||||||
|
const textFieldProps = {
|
||||||
|
gap: upMd ? "16px" : "10px",
|
||||||
|
color: "#F2F3F7",
|
||||||
|
bold: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return(
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridAutoFlow: upSm ? "column" : "row",
|
||||||
|
gridTemplateRows: "repeat(4, auto)",
|
||||||
|
gridAutoColumns: "1fr",
|
||||||
|
rowGap: "15px",
|
||||||
|
columnGap: "31px",
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputTextfield
|
||||||
|
TextfieldProps={{
|
||||||
|
placeholder: "Имя",
|
||||||
|
value: fields.firstname.value || "",
|
||||||
|
helperText: fields.firstname.touched && fields.firstname.error,
|
||||||
|
error: fields.firstname.touched && Boolean(fields.firstname.error),
|
||||||
|
}}
|
||||||
|
onChange={(e) => setSettingsField("firstname", e.target.value)}
|
||||||
|
id="firstname"
|
||||||
|
label="Имя"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
|
<InputTextfield
|
||||||
|
TextfieldProps={{
|
||||||
|
placeholder: "Фамилия",
|
||||||
|
value: fields.secondname.value || "",
|
||||||
|
helperText: fields.secondname.touched && fields.secondname.error,
|
||||||
|
error: fields.secondname.touched && Boolean(fields.secondname.error),
|
||||||
|
}}
|
||||||
|
onChange={(e) => setSettingsField("secondname", e.target.value)}
|
||||||
|
id="secondname"
|
||||||
|
label="Фамилия"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
|
<InputTextfield
|
||||||
|
TextfieldProps={{
|
||||||
|
placeholder: "Отчество",
|
||||||
|
value: fields.middlename.value || "",
|
||||||
|
helperText: fields.middlename.touched && fields.middlename.error,
|
||||||
|
error: fields.middlename.touched && Boolean(fields.middlename.error),
|
||||||
|
}}
|
||||||
|
onChange={(e) => setSettingsField("middlename", e.target.value)}
|
||||||
|
id="middlename"
|
||||||
|
label="Отчество"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
|
<InputTextfield
|
||||||
|
TextfieldProps={{
|
||||||
|
placeholder: "ООО Фирма",
|
||||||
|
value: fields.orgname.value || "",
|
||||||
|
helperText: fields.orgname.touched && fields.orgname.error,
|
||||||
|
error: fields.orgname.touched && Boolean(fields.orgname.error),
|
||||||
|
}}
|
||||||
|
onChange={(e) => setSettingsField("orgname", e.target.value)}
|
||||||
|
id="orgname"
|
||||||
|
label="Название компании"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
|
<InputTextfield
|
||||||
|
TextfieldProps={{
|
||||||
|
placeholder: "username@penahaub.com",
|
||||||
|
value: fields.email.value || "",
|
||||||
|
helperText: fields.email.touched && fields.email.error,
|
||||||
|
error: fields.email.touched && Boolean(fields.email.error),
|
||||||
|
}}
|
||||||
|
onChange={(e) => setSettingsField("email", e.target.value)}
|
||||||
|
id="email"
|
||||||
|
label="E-mail"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
|
<InputTextfield
|
||||||
|
TextfieldProps={{
|
||||||
|
placeholder: "+7 900 000 00 00",
|
||||||
|
value: fields.phoneNumber.value || "",
|
||||||
|
helperText: fields.phoneNumber.touched && fields.phoneNumber.error,
|
||||||
|
error: fields.phoneNumber.touched && Boolean(fields.phoneNumber.error),
|
||||||
|
}}
|
||||||
|
onChange={(e) => setSettingsField("phoneNumber", e.target.value)}
|
||||||
|
id="phoneNumber"
|
||||||
|
label="Телефон"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
TextfieldProps={{
|
||||||
|
placeholder: "Не менее 8 символов",
|
||||||
|
value: fields.password.value || "",
|
||||||
|
helperText: fields.password.touched && fields.password.error,
|
||||||
|
error: fields.password.touched && Boolean(fields.password.error),
|
||||||
|
autoComplete: "new-password",
|
||||||
|
}}
|
||||||
|
onChange={(e) => setSettingsField("password", e.target.value)}
|
||||||
|
id="password"
|
||||||
|
label="Пароль"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
@ -6,8 +6,10 @@ import CustomWrapper from "./CustomWrapper";
|
|||||||
import { useCart } from "@root/utils/hooks/useCart";
|
import { useCart } from "@root/utils/hooks/useCart";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
|
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
|
||||||
export default function Cart() {
|
function Cart() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(550));
|
const isMobile = useMediaQuery(theme.breakpoints.down(550));
|
||||||
@ -71,3 +73,8 @@ export default function Cart() {
|
|||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(Cart, {
|
||||||
|
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>,
|
||||||
|
onError: handleComponentError,
|
||||||
|
})
|
||||||
|
@ -11,6 +11,8 @@ import { HISTORY } from "./historyMocks";
|
|||||||
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
|
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
|
||||||
import { useHistoryData } from "@root/utils/hooks/useHistoryData";
|
import { useHistoryData } from "@root/utils/hooks/useHistoryData";
|
||||||
import { isArray } from "cypress/types/lodash";
|
import { isArray } from "cypress/types/lodash";
|
||||||
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
|
||||||
const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"];
|
const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"];
|
||||||
|
|
||||||
@ -67,6 +69,12 @@ export default function History() {
|
|||||||
) : (
|
) : (
|
||||||
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
||||||
)}
|
)}
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={
|
||||||
|
<Typography mt="8px">Ошибка загрузки истории</Typography>
|
||||||
|
}
|
||||||
|
onError={handleComponentError}
|
||||||
|
>
|
||||||
{historyData?.records
|
{historyData?.records
|
||||||
.filter((e) => {
|
.filter((e) => {
|
||||||
e.createdAt = extractDateFromString(e.createdAt)
|
e.createdAt = extractDateFromString(e.createdAt)
|
||||||
@ -84,6 +92,7 @@ export default function History() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)})}
|
)})}
|
||||||
|
</ErrorBoundary>
|
||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@ import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
|
|||||||
import SaveWrapper from "./SaveWrapper";
|
import SaveWrapper from "./SaveWrapper";
|
||||||
import { useTariffStore } from "@root/stores/tariffs";
|
import { useTariffStore } from "@root/stores/tariffs";
|
||||||
import { type Tariff } from "@frontend/kitui";
|
import { type Tariff } from "@frontend/kitui";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
|
||||||
export default function SavedTariffs() {
|
function SavedTariffs() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(550));
|
const isMobile = useMediaQuery(theme.breakpoints.down(550));
|
||||||
@ -65,3 +67,8 @@ export default function SavedTariffs() {
|
|||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(SavedTariffs, {
|
||||||
|
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении сохраненных тарифов</Typography>,
|
||||||
|
onError: handleComponentError,
|
||||||
|
})
|
||||||
|
@ -35,8 +35,10 @@ import {
|
|||||||
useTicketMessages,
|
useTicketMessages,
|
||||||
} from "@frontend/kitui";
|
} from "@frontend/kitui";
|
||||||
import { shownMessage, sendTicketMessage } from "@root/api/ticket";
|
import { shownMessage, sendTicketMessage } from "@root/api/ticket";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
|
||||||
export default function SupportChat() {
|
function SupportChat() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.up(460));
|
const isMobile = useMediaQuery(theme.breakpoints.up(460));
|
||||||
@ -128,7 +130,7 @@ export default function SupportChat() {
|
|||||||
async function handleSendMessage() {
|
async function handleSendMessage() {
|
||||||
if (!ticket || !messageField) return;
|
if (!ticket || !messageField) return;
|
||||||
|
|
||||||
const [_, sendTicketMessageError] = await sendTicketMessage(
|
const [, sendTicketMessageError] = await sendTicketMessage(
|
||||||
ticket.id,
|
ticket.id,
|
||||||
messageField
|
messageField
|
||||||
);
|
);
|
||||||
@ -318,3 +320,8 @@ export default function SupportChat() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(SupportChat, {
|
||||||
|
fallback: <Typography mt="8px" textAlign="center">Не удалось отобразить чат</Typography>,
|
||||||
|
onError: handleComponentError,
|
||||||
|
})
|
||||||
|
@ -5,13 +5,16 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
useTheme,
|
useTheme,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import TicketCard from "./TicketCard";
|
import TicketCard from "./TicketCard";
|
||||||
import { setTicketApiPage, useTicketStore } from "@root/stores/tickets";
|
import { setTicketApiPage, useTicketStore } from "@root/stores/tickets";
|
||||||
import { Ticket } from "@frontend/kitui";
|
import { Ticket } from "@frontend/kitui";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
|
||||||
|
|
||||||
export default function TicketList() {
|
function TicketList() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const tickets = useTicketStore((state) => state.tickets);
|
const tickets = useTicketStore((state) => state.tickets);
|
||||||
const ticketCount = useTicketStore((state) => state.ticketCount);
|
const ticketCount = useTicketStore((state) => state.ticketCount);
|
||||||
@ -87,3 +90,8 @@ function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) {
|
|||||||
const date2 = new Date(ticket2.updated_at).getTime();
|
const date2 = new Date(ticket2.updated_at).getTime();
|
||||||
return date2 - date1;
|
return date2 - date1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(TicketList, {
|
||||||
|
fallback: <Typography mt="8px" textAlign="center">Ошибка загрузки тикетов</Typography>,
|
||||||
|
onError: handleComponentError,
|
||||||
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box, IconButton, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import SectionWrapper from "@components/SectionWrapper";
|
import SectionWrapper from "@components/SectionWrapper";
|
||||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||||
@ -8,8 +8,10 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
|||||||
import TotalPrice from "@root/components/TotalPrice";
|
import TotalPrice from "@root/components/TotalPrice";
|
||||||
import { serviceNameByKey } from "@root/utils/serviceKeys";
|
import { serviceNameByKey } from "@root/utils/serviceKeys";
|
||||||
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
|
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
|
||||||
export default function TariffConstructor() {
|
function TariffConstructor() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
@ -84,3 +86,8 @@ export default function TariffConstructor() {
|
|||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(TariffConstructor, {
|
||||||
|
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении кастомных тарифов</Typography>,
|
||||||
|
onError: handleComponentError,
|
||||||
|
})
|
||||||
|
@ -28,6 +28,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
|
|||||||
const discounts = useDiscountStore((state) => state.discounts);
|
const discounts = useDiscountStore((state) => state.discounts);
|
||||||
const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts);
|
const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts);
|
||||||
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
|
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
|
||||||
|
const isUserNko = useUserStore(state => state.userAccount?.status) === "nko";
|
||||||
const [value, setValue] = useState<number>(userValue);
|
const [value, setValue] = useState<number>(userValue);
|
||||||
const throttledValue = useThrottle(value, 200);
|
const throttledValue = useThrottle(value, 200);
|
||||||
|
|
||||||
@ -39,10 +40,11 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
|
|||||||
throttledValue,
|
throttledValue,
|
||||||
discounts,
|
discounts,
|
||||||
currentCartTotal,
|
currentCartTotal,
|
||||||
purchasesAmount
|
purchasesAmount,
|
||||||
|
isUserNko,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue]
|
[currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue, isUserNko]
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSliderChange(value: number | number[]) {
|
function handleSliderChange(value: number | number[]) {
|
||||||
|
@ -18,6 +18,8 @@ import { Slider } from "./slider";
|
|||||||
import { useCartStore } from "@root/stores/cart";
|
import { useCartStore } from "@root/stores/cart";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
|
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
|
||||||
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleComponentError } from "@root/utils/handleComponentError";
|
||||||
|
|
||||||
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
|
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
|
||||||
|
|
||||||
@ -26,7 +28,7 @@ const StepperText: Record<string, string> = {
|
|||||||
time: "Тарифы на время",
|
time: "Тарифы на время",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TariffPage() {
|
function TariffPage() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
@ -178,3 +180,8 @@ export default function TariffPage() {
|
|||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(TariffPage, {
|
||||||
|
fallback: <Typography mt="8px" textAlign="center">Ошибка загрузки тарифов</Typography>,
|
||||||
|
onError: handleComponentError,
|
||||||
|
})
|
||||||
|
@ -5,6 +5,7 @@ import { produce } from "immer";
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { devtools, persist } from "zustand/middleware";
|
import { devtools, persist } from "zustand/middleware";
|
||||||
import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
||||||
|
import { findNkoDiscount } from "@root/utils/calcCart/calcCart";
|
||||||
|
|
||||||
|
|
||||||
interface CustomTariffsStore {
|
interface CustomTariffsStore {
|
||||||
@ -50,6 +51,7 @@ export const setCustomTariffsUserValue = (
|
|||||||
discounts: Discount[],
|
discounts: Discount[],
|
||||||
currentCartTotal: number,
|
currentCartTotal: number,
|
||||||
purchasesAmount: number,
|
purchasesAmount: number,
|
||||||
|
isUserNko: boolean,
|
||||||
) => useCustomTariffsStore.setState(
|
) => useCustomTariffsStore.setState(
|
||||||
produce<CustomTariffsStore>(state => {
|
produce<CustomTariffsStore>(state => {
|
||||||
state.userValuesMap[serviceKey] ??= {};
|
state.userValuesMap[serviceKey] ??= {};
|
||||||
@ -58,6 +60,16 @@ export const setCustomTariffsUserValue = (
|
|||||||
let priceWithoutDiscounts = 0;
|
let priceWithoutDiscounts = 0;
|
||||||
let priceAfterDiscounts = 0;
|
let priceAfterDiscounts = 0;
|
||||||
|
|
||||||
|
const nkoDiscount = findNkoDiscount(discounts);
|
||||||
|
|
||||||
|
if (isUserNko && nkoDiscount) {
|
||||||
|
state.privilegeByService[serviceKey].forEach(privilege => {
|
||||||
|
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
|
||||||
|
priceWithoutDiscounts += privilege.price * amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
priceAfterDiscounts = priceWithoutDiscounts * findDiscountFactor(nkoDiscount);
|
||||||
|
} else {
|
||||||
state.privilegeByService[serviceKey].forEach(privilege => {
|
state.privilegeByService[serviceKey].forEach(privilege => {
|
||||||
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
|
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
|
||||||
priceWithoutDiscounts += privilege.price * amount;
|
priceWithoutDiscounts += privilege.price * amount;
|
||||||
@ -74,6 +86,7 @@ export const setCustomTariffsUserValue = (
|
|||||||
|
|
||||||
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
||||||
priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount);
|
priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount);
|
||||||
|
}
|
||||||
|
|
||||||
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
|
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
|
||||||
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
|
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
|
||||||
|
@ -23,7 +23,7 @@ interface UserStore {
|
|||||||
verificationStatus: VerificationStatus;
|
verificationStatus: VerificationStatus;
|
||||||
verificationType: "juridical" | "nko";
|
verificationType: "juridical" | "nko";
|
||||||
isDocumentsDialogOpen: boolean;
|
isDocumentsDialogOpen: boolean;
|
||||||
dialogType: "juridical" | "nko";
|
dialogType: "juridical" | "nko" | "";
|
||||||
documents: UserDocuments;
|
documents: UserDocuments;
|
||||||
documentsUrl: UserDocumentsUrl;
|
documentsUrl: UserDocumentsUrl;
|
||||||
comment: string;
|
comment: string;
|
||||||
@ -61,7 +61,7 @@ const initialState: UserStore = {
|
|||||||
verificationStatus: VerificationStatus.NOT_VERIFICATED,
|
verificationStatus: VerificationStatus.NOT_VERIFICATED,
|
||||||
verificationType: "juridical",
|
verificationType: "juridical",
|
||||||
isDocumentsDialogOpen: false,
|
isDocumentsDialogOpen: false,
|
||||||
dialogType: "juridical",
|
dialogType: "",
|
||||||
comment: "",
|
comment: "",
|
||||||
documents: {
|
documents: {
|
||||||
ИНН: { ...defaultDocument },
|
ИНН: { ...defaultDocument },
|
||||||
@ -87,7 +87,7 @@ export const useUserStore = create<UserStore>()(
|
|||||||
version: 2,
|
version: 2,
|
||||||
name: "user",
|
name: "user",
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({ // список полей для хранения в ЛС
|
||||||
userId: state.userId,
|
userId: state.userId,
|
||||||
user: state.user,
|
user: state.user,
|
||||||
}),
|
}),
|
||||||
@ -161,6 +161,7 @@ export const closeDocumentsDialog = () =>
|
|||||||
useUserStore.setState(
|
useUserStore.setState(
|
||||||
produce<UserStore>((state) => {
|
produce<UserStore>((state) => {
|
||||||
state.isDocumentsDialogOpen = false;
|
state.isDocumentsDialogOpen = false;
|
||||||
|
state.dialogType = "";
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -278,7 +279,7 @@ export const sendUserData = async () => {
|
|||||||
export const addTariffToCart = async (tariffId: string) => {
|
export const addTariffToCart = async (tariffId: string) => {
|
||||||
const [patchCartResponse, patchCartError] = await patchCart(tariffId);
|
const [patchCartResponse, patchCartError] = await patchCart(tariffId);
|
||||||
|
|
||||||
if (patchCartError !== undefined) {
|
if (patchCartError === undefined) {
|
||||||
setCart(patchCartResponse);
|
setCart(patchCartResponse);
|
||||||
}
|
}
|
||||||
return({patchCartResponse, patchCartError})
|
return({patchCartResponse, patchCartError})
|
||||||
|
@ -75,7 +75,7 @@ function applyNkoDiscount(cartData: CartData, discount: Discount) {
|
|||||||
cartData.allAppliedDiscounts.push(discount);
|
cartData.allAppliedDiscounts.push(discount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNkoDiscount(discounts: Discount[]): Discount | null {
|
export function findNkoDiscount(discounts: Discount[]): Discount | null {
|
||||||
const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko");
|
const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko");
|
||||||
|
|
||||||
if (!applicableDiscounts.length) return null;
|
if (!applicableDiscounts.length) return null;
|
||||||
|
44
src/utils/handleComponentError.ts
Normal file
44
src/utils/handleComponentError.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
import { ErrorInfo } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
interface ComponentError {
|
||||||
|
timestamp: number;
|
||||||
|
message: string;
|
||||||
|
callStack: string | undefined;
|
||||||
|
componentStack: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleComponentError(error: Error, info: ErrorInfo) {
|
||||||
|
const componentError: ComponentError = {
|
||||||
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
message: error.message,
|
||||||
|
callStack: error.stack,
|
||||||
|
componentStack: info.componentStack,
|
||||||
|
};
|
||||||
|
|
||||||
|
queueErrorRequest(componentError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorsQueue: ComponentError[] = [];
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
function queueErrorRequest(error: ComponentError) {
|
||||||
|
errorsQueue.push(error);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
sendErrorsToServer();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendErrorsToServer() {
|
||||||
|
// makeRequest({
|
||||||
|
// url: "",
|
||||||
|
// method: "POST",
|
||||||
|
// body: errorsQueue,
|
||||||
|
// useToken: true,
|
||||||
|
// });
|
||||||
|
console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
|
||||||
|
errorsQueue = [];
|
||||||
|
}
|
15
yarn.lock
15
yarn.lock
@ -1532,10 +1532,10 @@
|
|||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
"@frontend/kitui@1.0.53":
|
"@frontend/kitui@1.0.54":
|
||||||
version "1.0.53"
|
version "1.0.54"
|
||||||
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.53.tgz#a663052d300b9e3c588346c646f276c9aec7de5d"
|
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.54.tgz#0235d5a8effb9b92351471c3c7775f28cb2839f6"
|
||||||
integrity sha1-pmMFLTALnjxYg0bGRvJ2ya7H3l0=
|
integrity sha1-AjXVqO/7m5I1FHHDx3dfKMsoOfY=
|
||||||
dependencies:
|
dependencies:
|
||||||
immer "^10.0.2"
|
immer "^10.0.2"
|
||||||
reconnecting-eventsource "^1.6.2"
|
reconnecting-eventsource "^1.6.2"
|
||||||
@ -9448,6 +9448,13 @@ react-dom@^18.2.0:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.0"
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
|
react-error-boundary@^4.0.11:
|
||||||
|
version "4.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c"
|
||||||
|
integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.12.5"
|
||||||
|
|
||||||
react-error-overlay@^6.0.11:
|
react-error-overlay@^6.0.11:
|
||||||
version "6.0.11"
|
version "6.0.11"
|
||||||
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"
|
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user