Merge branch 'dev' into staging

This commit is contained in:
Nastya 2024-04-28 01:41:21 +03:00
commit 4efadbc628
14 changed files with 259 additions and 119 deletions

@ -36,6 +36,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta http-equiv="Pragma" content="no-cache" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap" rel="stylesheet" />

@ -117,6 +117,7 @@ export interface QuizConfig {
};
meta: string;
yandexMetricNumber: number | undefined;
vkMetricNumber: number | undefined;
}
export type FormContactFieldName =
@ -225,4 +226,5 @@ export const defaultQuizConfig: QuizConfig = {
},
meta: "",
yandexMetricNumber: undefined,
vkMetricNumber: undefined,
};

@ -116,33 +116,32 @@ const GeneralItemTimeConv = ({
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(700));
const data = Object.entries(general)
.sort((a, b) => a[0] - b[0]);
const data = Object.entries(general).sort((a, b) => a[0] - b[0]);
const days = [...data].map(e => e[0])
const days = [...data].map((e) => e[0]);
let buffer = 0
let buffer = 0;
const time = [...data].map(e => {
const time = [...data].map((e) => {
if (e[1] > 0) {
buffer = e[1]
buffer = e[1];
}
return buffer
})
return buffer;
});
console.log("data", data)
console.log("time", time.reduce((a, b) => (Number(a) + Number(b)), 0))
console.log("time", getCalculatedTime(time.reduce((a, b) => (Number(a) + Number(b)), 0)))
console.log("days", days.length)
const numberValue = calculateTime ?
(
(time.reduce((a, b) => (Number(a) + Number(b)), 0))
/
(days.length)
) || 0
:
conversionValue
console.log("data", data);
console.log(
"time",
time.reduce((a, b) => Number(a) + Number(b), 0),
);
console.log(
"time",
getCalculatedTime(time.reduce((a, b) => Number(a) + Number(b), 0)),
);
console.log("days", days.length);
const numberValue = calculateTime
? time.reduce((a, b) => Number(a) + Number(b), 0) / days.length || 0
: conversionValue;
if (
Object.keys(general).length === 0 ||
@ -153,7 +152,6 @@ const GeneralItemTimeConv = ({
);
}
return (
<Paper
sx={{
@ -164,7 +162,9 @@ const GeneralItemTimeConv = ({
>
<Typography sx={{ margin: "20px 20px 0" }}>{title}</Typography>
<Typography sx={{ margin: "10px 20px 0", fontWeight: "bold" }}>
{calculateTime ? `${getCalculatedTime(numberValue)} с` : `${numberValue.toFixed(2)}%`}
{calculateTime
? `${getCalculatedTime(numberValue)} с`
: `${numberValue.toFixed(2)}%`}
</Typography>
<LineChart
xAxis={[
@ -178,10 +178,11 @@ const GeneralItemTimeConv = ({
{
data: Object.values(time),
valueFormatter: (value) => {
console.log("log", value)
return calculateTime ? getCalculatedTime(value) : String((value*100).toFixed(2)) + "%"
}
,
console.log("log", value);
return calculateTime
? getCalculatedTime(value)
: String((value * 100).toFixed(2)) + "%";
},
},
]}
// dataset={Object.entries(general).map(([, v]) => moment.unix(v).format("ss:mm:HH")).reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})}
@ -196,7 +197,6 @@ const GeneralItemTimeConv = ({
);
};
export const General: FC<GeneralProps> = ({ data, day }) => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));

@ -1,6 +1,6 @@
import { Box, Typography, useTheme } from "@mui/material";
import { Box, useTheme } from "@mui/material";
import { FC } from "react";
import YandexMetric from "../mocks/YandexMetric.png";
import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
type PartnerItemProps = {
setIsModalOpen: (value: boolean) => void;
@ -34,7 +34,7 @@ export const YandexButton: FC<PartnerItemProps> = ({
}}
onClick={() => setIsModalOpen(true)}
>
<img width={"100%"} src={YandexMetric} alt={"Yandex.Метрика"} />
<YandexMetricaLogo />
</Box>
</>
);

@ -30,13 +30,24 @@ export default function YandexModal({ isModalOpen, handleCloseModal }: Props) {
const [currentValue, setCurrentValue] = useState<string>(
yandexNumber ? yandexNumber.toString() : "",
);
const handleClose = () => {
handleCloseModal();
if (!yandexNumber) {
setIsSave(false);
setCurrentValue("");
return;
}
setIsSave(true);
setCurrentValue(yandexNumber.toString());
};
const handleSave = () => {
handleCloseModal();
updateQuiz(quiz?.id, (quiz) => {
quiz.config.yandexMetricNumber = currentValue
? Number(currentValue)
: undefined;
});
handleCloseModal();
if (!currentValue) {
setIsSave(false);
return;
@ -55,7 +66,7 @@ export default function YandexModal({ isModalOpen, handleCloseModal }: Props) {
return (
<Dialog
open={isModalOpen}
onClose={handleCloseModal}
onClose={handleClose}
fullWidth
PaperProps={{
sx: {
@ -83,7 +94,7 @@ export default function YandexModal({ isModalOpen, handleCloseModal }: Props) {
</Typography>
</Box>
<IconButton
onClick={handleCloseModal}
onClick={handleClose}
sx={{
width: "12px",
height: "12px",
@ -166,7 +177,7 @@ export default function YandexModal({ isModalOpen, handleCloseModal }: Props) {
>
<Button
sx={{ width: isMobile ? "100%" : "130px" }}
onClick={handleCloseModal}
onClick={handleClose}
variant={"outlined"}
>
Отмена

@ -0,0 +1,6 @@
import React from "react";
import { ReactComponent as YandexLogo } from "./yandexMetricaLogo.svg";
export const YandexMetricaLogo = () => {
return <YandexLogo />;
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 168 KiB

@ -8,7 +8,7 @@ import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import { useParams } from "react-router-dom";
import useSWR from "swr";
import { useYandexMetrica } from "@utils/hooks/useYandexMetrica";
import { useYandexMetrics } from "@utils/hooks/useYandexMetrics";
export default function ViewPublicationPage() {
const quizId = useParams().quizId;
@ -23,7 +23,7 @@ export default function ViewPublicationPage() {
const quiz = quizes?.find((quiz) => quiz.qid === quizId);
const yandexMetricNumber = quiz?.config.yandexMetricNumber;
useYandexMetrica(yandexMetricNumber);
useYandexMetrics(yandexMetricNumber);
const {
data: rawQuestions,

@ -41,7 +41,17 @@ export const setQuestions = (questions: RawQuestion[] | null | undefined) =>
export const createUntypedQuestion = (
quizId: number,
insertAfterQuestionId?: string,
) =>
) => {
const { questions } = useQuestionsStore.getState();
const questionsAmount = questions.filter(
({ type }) => type !== "result",
).length;
if (questionsAmount >= 100) {
return;
}
setProducedState(
(state) => {
const newUntypedQuestion = {
@ -70,6 +80,7 @@ export const createUntypedQuestion = (
quizId,
},
);
};
const removeQuestion = (questionId: string) =>
setProducedState(
@ -530,66 +541,80 @@ export const deleteQuestion = async (questionId: string) =>
}
});
export const copyQuestion = async (questionId: string, quizId: number) =>
requestQueue.enqueue(`copyQuestion-${quizId}-${questionId}`, async () => {
const question = useQuestionsStore
.getState()
.questions.find((q) => q.id === questionId);
if (!question) return;
export const copyQuestion = async (questionId: string, quizId: number) => {
const { questions } = useQuestionsStore.getState();
const frontId = nanoid();
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = frontId;
const questionsAmount = questions.filter(
({ type }) => type !== "result",
).length;
setProducedState(
(state) => {
state.questions.push(copiedQuestion);
},
{
type: "copyQuestion",
questionId,
if (questionsAmount >= 100) {
return;
}
return requestQueue.enqueue(
`copyQuestion-${quizId}-${questionId}`,
async () => {
const question = useQuestionsStore
.getState()
.questions.find((q) => q.id === questionId);
if (!question) return;
const frontId = nanoid();
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = frontId;
setProducedState(
(state) => {
state.questions.push(copiedQuestion);
},
{
type: "copyQuestion",
questionId,
quizId,
},
);
return;
}
try {
const { updated: newQuestionId } = await questionApi.copy(
question.backendId,
quizId,
},
);
);
return;
}
const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
copiedQuestion.content.rule = {
main: [],
parentId: "",
default: "",
children: [],
};
try {
const { updated: newQuestionId } = await questionApi.copy(
question.backendId,
quizId,
);
setProducedState(
(state) => {
state.questions.push(copiedQuestion);
},
{
type: "copyQuestion",
questionId,
quizId,
},
);
const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
copiedQuestion.content.rule = {
main: [],
parentId: "",
default: "",
children: [],
};
setProducedState(
(state) => {
state.questions.push(copiedQuestion);
},
{
type: "copyQuestion",
questionId,
quizId,
},
);
updateQuestionOrders();
} catch (error) {
devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос");
}
});
updateQuestionOrders();
} catch (error) {
devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос");
}
},
);
};
function setProducedState<A extends string | { type: string }>(
recipe: (state: QuestionsStore) => void,

@ -54,6 +54,10 @@ export const cleanAuthTicketData = () => {
useTicketStore.setState({ authData: initAuthData });
};
export const cleanUnauthTicketData = () => {
useTicketStore.setState({ unauthData: initAuthData });
};
export const setTicketData = (sessionData: SessionData) =>
updateTicket((ticket) => {
ticket.sessionData = sessionData;

@ -11,36 +11,37 @@ import {
useTheme,
} from "@mui/material";
import {
useTicketStore,
addOrUpdateUnauthMessages,
incrementUnauthMessage,
setUnauthIsPreventAutoscroll,
useTicketStore,
} from "@root/ticket";
import type { TouchEvent, WheelEvent } from "react";
import * as React from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import ChatMessage from "./ChatMessage";
import ChatVideo from "./ChatVideo";
import SendIcon from "@icons/SendIcon";
import UserCircleIcon from "./UserCircleIcon";
import { throttle } from "@frontend/kitui";
import { throttle, TicketMessage } from "@frontend/kitui";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { useUserStore } from "@root/user";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import ChatImage from "./ChatImage";
import ChatDocument from "@ui_kit/FloatingSupportChat/ChatDocument";
import * as React from "react";
import {
checkAcceptableMediaType,
ACCEPT_SEND_MEDIA_TYPES_MAP,
checkAcceptableMediaType,
} from "@utils/checkAcceptableMediaType";
import { enqueueSnackbar } from "notistack";
import type { WheelEvent, TouchEvent } from "react";
interface Props {
open: boolean;
sx?: SxProps<Theme>;
onclickArrow?: () => void;
sendMessage: (a: string) => Promise<boolean>;
sendFile: (a: File | undefined) => Promise<true>;
greetingMessage: TicketMessage;
}
export default function Chat({
@ -49,6 +50,7 @@ export default function Chat({
onclickArrow,
sendMessage,
sendFile,
greetingMessage,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
@ -69,6 +71,13 @@ export default function Chat({
const chatBoxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
addOrUpdateUnauthMessages([greetingMessage]);
if (open) {
scrollToBottom();
}
}, [open]);
const sendMessageHC = async () => {
const successful = await sendMessage(messageField);
if (successful) {
@ -193,7 +202,7 @@ export default function Chat({
color: theme.palette.common.white,
}}
>
<Typography>Мария</Typography>
<Typography>Данила</Typography>
<Typography
sx={{
fontSize: "16px",
@ -202,6 +211,14 @@ export default function Chat({
>
онлайн-консультант
</Typography>
<Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
время работы 10:00-3:00 по мск
</Typography>
</Box>
</Box>
<Box
@ -310,6 +327,17 @@ export default function Chat({
/>
);
})}
{!ticket.sessionData?.ticketId && (
<ChatMessage
unAuthenticated
text={greetingMessage.message}
createdAt={greetingMessage.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
greetingMessage.user_id
}
/>
)}
</Box>
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
<InputBase

@ -1,14 +1,15 @@
import { useState, useEffect, forwardRef } from "react";
import type { ReactNode, Ref } from "react";
import { forwardRef, useEffect, useState } from "react";
import {
Box,
Fab,
useTheme,
useMediaQuery,
Slide,
Dialog,
Badge,
Box,
Dialog,
Fab,
Modal,
Slide,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import CircleDoubleDown from "./QuestionIcon";
import Chat from "./Chat";
@ -16,9 +17,7 @@ import { TransitionProps } from "@mui/material/transitions";
import { useLocation } from "react-router-dom";
import { useTicketStore } from "@root/ticket";
import { useUserStore } from "@root/user";
import { ACCEPT_SEND_FILE_TYPES_MAP } from "@frontend/squzanswerer/dist-package/components/ViewPublicationPage/tools/fileUpload";
import type { ReactNode, Ref } from "react";
import { TicketMessage } from "@frontend/kitui";
const animation = {
"@keyframes runningStripe": {
@ -48,6 +47,7 @@ interface Props {
sendFile: (a: File | undefined) => Promise<true>;
modalWarningType: string | null;
setModalWarningType: any;
greetingMessage: TicketMessage;
}
export default function FloatingSupportChat({
@ -59,6 +59,7 @@ export default function FloatingSupportChat({
sendFile,
modalWarningType,
setModalWarningType,
greetingMessage,
}: Props) {
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
const theme = useTheme();
@ -107,6 +108,7 @@ export default function FloatingSupportChat({
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
sendMessage={sendMessage}
sendFile={sendFile}
greetingMessage={greetingMessage}
/>
<Dialog
fullScreen
@ -119,6 +121,7 @@ export default function FloatingSupportChat({
onclickArrow={handleChatClickClose}
sendMessage={sendMessage}
sendFile={sendFile}
greetingMessage={greetingMessage}
/>
</Dialog>
<Fab

@ -1,4 +1,5 @@
import {
createTicket,
TicketMessage,
useSSESubscription,
useTicketMessages,
@ -7,21 +8,21 @@ import {
import makeRequest from "@api/makeRequest";
import FloatingSupportChat from "./FloatingSupportChat";
import { useUserStore } from "@root/user";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { sendTicketMessage, shownMessage } from "../../api/ticket";
import { useSSETab } from "../../utils/hooks/useSSETab";
import {
addOrUpdateUnauthMessages,
useTicketStore,
cleanAuthTicketData,
cleanUnauthTicketData,
setIsMessageSending,
setTicketData,
setUnauthIsPreventAutoscroll,
setUnauthTicketMessageFetchState,
cleanAuthTicketData,
setTicketData,
useTicketStore,
} from "@root/ticket";
import { enqueueSnackbar } from "notistack";
import { getMessageFromFetchError } from "@utils/backendMessageHandler";
import { createTicket } from "@frontend/kitui";
import { setIsMessageSending } from "@root/ticket";
type ModalWarningType =
| "errorType"
@ -59,6 +60,8 @@ export default () => {
const [modalWarningType, setModalWarningType] =
useState<ModalWarningType>(null);
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
const [sseEnabled, setSseEnabled] = useState(true);
const handleChatClickOpen = () => {
setIsChatOpened(true);
};
@ -68,6 +71,34 @@ export default () => {
const handleChatClickSwitch = () => {
setIsChatOpened((state) => !state);
};
const getGreetingMessage: TicketMessage = useMemo(() => {
const workingHoursMessage =
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
const offHoursMessage =
"Здравствуйте, к сожалению, сейчас операторы не работают. Задайте ваш вопрос, и вам ответят в 10:00 по московскому времени";
const date = new Date();
const currentHourUTC = date.getUTCHours();
const MscTime = 3; // Москва UTC+3;
const moscowHour = (currentHourUTC + MscTime) % 24;
const greetingMessage =
moscowHour >= 10 && moscowHour < 15
? workingHoursMessage
: offHoursMessage;
return {
created_at: new Date().toISOString(),
files: [],
id: "111",
message: greetingMessage,
request_screenshot: "",
session_id: "greetingMessage",
shown: { me: 1 },
ticket_id: "111",
user_id: "greetingMessage",
};
}, [isChatOpened]);
useTicketsFetcher({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets",
ticketsPerPage: 10,
@ -113,22 +144,37 @@ export default () => {
});
useSSESubscription<TicketMessage>({
enabled: isActiveSSETab && Boolean(ticket.sessionData?.sessionId),
enabled:
sseEnabled && isActiveSSETab && Boolean(ticket.sessionData?.sessionId),
url:
process.env.REACT_APP_DOMAIN +
`/heruvym/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`,
onNewData: (ticketMessages) => {
const isTicketClosed = ticketMessages.some(
(message) => message.session_id === "close",
);
if (isTicketClosed) {
cleanAuthTicketData();
addOrUpdateUnauthMessages([getGreetingMessage]);
if (!user) {
cleanUnauthTicketData();
localStorage.removeItem("unauth-ticket");
}
return;
}
updateSSEValue(ticketMessages);
addOrUpdateUnauthMessages(ticketMessages);
},
onDisconnect: useCallback(() => {
setUnauthIsPreventAutoscroll(false);
setSseEnabled(false);
}, []),
marker: "ticket",
});
useEffect(() => {
cleanAuthTicketData();
setSseEnabled(true);
}, [user]);
useEffect(() => {
@ -145,7 +191,7 @@ export default () => {
const sendMessage = async (messageField: string) => {
if (!messageField || ticket.isMessageSending) return false;
setSseEnabled(true);
let successful = false;
setIsMessageSending(true);
if (!ticket.sessionData?.ticketId) {
@ -245,6 +291,7 @@ export default () => {
sendFile={sendFile}
modalWarningType={modalWarningType}
setModalWarningType={setModalWarningType}
greetingMessage={getGreetingMessage}
/>
);
};

@ -1,8 +1,12 @@
import { useEffect } from "react";
export const useYandexMetrica = (yandexMetricNumber: number | undefined) => {
export const useYandexMetrics = (yandexMetricNumber: number | undefined) => {
useEffect(() => {
if (yandexMetricNumber) {
if (
yandexMetricNumber &&
typeof yandexMetricNumber === "number" &&
!Number.isNaN(yandexMetricNumber)
) {
const script = document.createElement("script");
script.type = "text/javascript";
script.innerHTML = `