fix results page

This commit is contained in:
Tatiana Nikolaeva 2025-06-10 08:42:07 +05:00
parent 3c466a98d3
commit 1e60984d64
9 changed files with 267 additions and 136 deletions

View file

@ -11,6 +11,10 @@ export const handleUnauthorizedError = (error: unknown) => {
window.location.href = '/login'; window.location.href = '/login';
console.log('Сессия истекла. Перенаправление на страницу входа.'); console.log('Сессия истекла. Перенаправление на страницу входа.');
} }
if (error instanceof Error && error.message.includes('Токен отсутствует')) {
window.location.href = '/login';
console.log('Сессия истекла. Перенаправление на страницу входа.');
}
}; };
/** /**

View file

@ -58,7 +58,8 @@ export const updateQuestion = async (id: number, question: Partial<INewQuestion>
questionType: question.questionType, questionType: question.questionType,
}), }),
}) })
return await handleResponse(response) const result = await handleResponse(response);
return result;
} }
catch(error){ catch(error){
handleUnauthorizedError(error); handleUnauthorizedError(error);

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import styles from './Account.module.css'; import styles from './Account.module.css';
import AccountImg from '../../assets/account.svg?react'; import AccountImg from '../../assets/account.svg?react';
import { getCurrentUser } from '../../api/AuthApi'; import { getCurrentUser } from '../../api/AuthApi';
import {handleUnauthorizedError} from "../../api/BaseApi.ts";
interface AccountProps { interface AccountProps {
href: string; href: string;
@ -17,6 +18,7 @@ const Account: React.FC<AccountProps> = ({ href }) => {
const userData = await getCurrentUser(); const userData = await getCurrentUser();
setUserName(`${userData.firstName} ${userData.lastName}`); setUserName(`${userData.firstName} ${userData.lastName}`);
} catch (error) { } catch (error) {
handleUnauthorizedError(error)
console.error("Ошибка загрузки данных пользователя:", error); console.error("Ошибка загрузки данных пользователя:", error);
localStorage.removeItem("user"); localStorage.removeItem("user");
} finally { } finally {

View file

@ -53,9 +53,9 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
setTextQuestion(valueQuestion); setTextQuestion(valueQuestion);
}, [valueQuestion]); }, [valueQuestion]);
useEffect(() => { // useEffect(() => {
setQuestionType(initialQuestionType); // setQuestionType(initialQuestionType);
}, [initialQuestionType]); // }, [initialQuestionType]);
const handleTypeChange = (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => { const handleTypeChange = (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => {
setQuestionType(type); setQuestionType(type);
@ -167,18 +167,6 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
} }
}; };
// const toggleSelect = (index: number) => {
// if (initialQuestionType === 'SingleAnswerQuestion') {
// setSelectedAnswers([index]);
// } else {
// setSelectedAnswers(prev =>
// prev.includes(index)
// ? prev.filter(i => i !== index)
// : [...prev, index]
// );
// }
// };
const toggleSelect = (index: number) => { const toggleSelect = (index: number) => {
const answerText = initialAnswerVariants[index].text; const answerText = initialAnswerVariants[index].text;
@ -234,7 +222,13 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2> <h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
</button> </button>
)} )}
<TypeDropdown selectedType={questionType} onTypeChange={handleTypeChange}/>
<TypeDropdown
selectedType={questionType}
onTypeChange={(type) => {
handleTypeChange(type);
}}
/>
</div> </div>
{initialAnswerVariants.map((answer, index) => ( {initialAnswerVariants.map((answer, index) => (

View file

@ -147,7 +147,6 @@
.exportButtonContainer { .exportButtonContainer {
padding: 10px 15px; padding: 10px 15px;
/*background-color: #4CAF50;*/
background-color: #3788D6; background-color: #3788D6;
color: white; color: white;
border: none; border: none;
@ -159,5 +158,52 @@
} }
.exportButtonContainer:hover { .exportButtonContainer:hover {
background-color: #45a049; background-color: rgb(14, 122, 221);
}
.info{
margin-top: 60px;
text-align: center;
}
.exportButtonWrapper {
position: relative;
display: inline-block;
width: 100%;
text-align: center;
}
.exportPopup {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255, 255, 255, 0.9);
color: #333;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
white-space: nowrap;
animation: fadeInOut 2s ease-in-out;
z-index: 10;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
20% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
80% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
100% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
} }

View file

@ -10,7 +10,7 @@ import Group from '../../assets/gmail_groups.svg?react';
import Send from '../../assets/send.svg?react'; import Send from '../../assets/send.svg?react';
import {getAllCompletions} from "../../api/CompletionApi.ts"; import {getAllCompletions} from "../../api/CompletionApi.ts";
import {getAnswer} from "../../api/AnswerApi.ts"; import {getAnswer} from "../../api/AnswerApi.ts";
import {useEffect, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {getListQuestions} from "../../api/QuestionApi.ts"; import {getListQuestions} from "../../api/QuestionApi.ts";
import {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts"; import {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts";
import {getResultsFile} from "../../api/ExportResultApi.ts"; import {getResultsFile} from "../../api/ExportResultApi.ts";
@ -52,10 +52,18 @@ export const Results = () => {
}); });
const [survey, setLocalSurvey] = useState<ISurvey>(initialSurvey); const [survey, setLocalSurvey] = useState<ISurvey>(initialSurvey);
// const [questions, setQuestions] = useState<IQuestion[]>([]);
const [showExportPopup, setShowExportPopup] = useState(false);
const exportButtonRef = useRef<HTMLButtonElement>(null);
const handleExportToExcel = async (id: number) => { const handleExportToExcel = async (id: number) => {
await getResultsFile(id) try {
await getResultsFile(id);
setShowExportPopup(true);
setTimeout(() => setShowExportPopup(false), 2000);
} catch (error) {
console.error('Ошибка при экспорте:', error);
}
}; };
useEffect(() => { useEffect(() => {
@ -65,7 +73,6 @@ export const Results = () => {
setLocalSurvey(surveyData); setLocalSurvey(surveyData);
const questionsList = await getListQuestions(survey.id); const questionsList = await getListQuestions(survey.id);
// setQuestions(questionsList);
const questionsWithVariants = await Promise.all( const questionsWithVariants = await Promise.all(
questionsList.map(async (question) => { questionsList.map(async (question) => {
@ -205,116 +212,134 @@ export const Results = () => {
</div> </div>
</div> </div>
{surveyStats.questions.map((question, index) => ( {surveyStats.totalParticipants === 0 ? (
<div key={index} className={styles.questionContainer}> <p className={styles.info}>Нет данных о прохождении опроса</p>
<div className={styles.questionContent}> ) : (
<div className={styles.textContainer}> surveyStats.questions.map((question, index) => (
<h3>{question.questionText}</h3> <div key={index} className={styles.questionContainer}>
<p className={styles.answerCount}>Ответов: {question.totalAnswers}</p> <div className={styles.questionContent}>
</div> <div className={styles.textContainer}>
<h3>{question.questionText}</h3>
<p className={styles.answerCount}>Ответов: {question.totalAnswers}</p>
</div>
<div className={styles.chartContainer}> <div className={styles.chartContainer}>
{question.isMultipleChoice ? ( {question.isMultipleChoice ? (
<div className={styles.barContainer}> <div className={styles.barContainer}>
<Bar <Bar
data={{ data={{
labels: question.options.map(opt => opt.text), labels: question.options.map(opt => opt.text),
datasets: [{ datasets: [{
label: '% выбравших', label: '% выбравших',
data: question.options.map(opt => opt.percentage), data: question.options.map(opt => opt.percentage),
backgroundColor: colorsForBar, backgroundColor: colorsForBar,
borderColor: colorsForBar, borderColor: colorsForBar,
borderWidth: 2, borderWidth: 2,
borderRadius: 8, borderRadius: 8,
borderSkipped: false, borderSkipped: false,
}] }]
}} }}
options={{ options={{
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: {
display: false display: false
}, },
tooltip: { enabled: true }, tooltip: { enabled: true },
datalabels: { display: false }, datalabels: { display: false },
annotation: { annotation: {
annotations: question.options.map((opt, i) => ({ annotations: question.options.map((opt, i) => ({
type: 'label', type: 'label',
xValue: i, xValue: i,
yValue: opt.percentage + 5, yValue: opt.percentage + 5,
content: `${opt.percentage}%`, content: `${opt.percentage}%`,
font: { size: 1, weight: 400 }, font: { size: 1, weight: 400 },
color: '#000' color: '#000'
})) }))
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: { callback: (val) => `${val}%` }
},
x: {
ticks: {
color: '#000000',
font: {
size: 12,
weight: 400
} }
}, },
grid: { display: false } scales: {
} y: {
} beginAtZero: true,
}} max: 100,
/> ticks: { callback: (val) => `${val}%` }
</div> },
) : ( x: {
<div className={styles.pieContainer}> ticks: {
<Pie color: '#000000',
data={{ font: {
labels: question.options.map(opt => opt.text), size: 12,
datasets: [{ weight: 400
data: question.options.map(opt => opt.percentage), }
backgroundColor: colorsForPie, },
borderColor: '#fff', grid: { display: false }
borderWidth: 2
}]
}}
options={{
responsive: true,
plugins: {
legend: {
position: 'right',
labels: {
color: '#000000',
font: {
size: 12,
weight: 500
} }
} }
}, }}
tooltip: { />
callbacks: { </div>
label: (ctx) => `${ctx.label}: ${ctx.raw}%` ) : (
} <div className={styles.pieContainer}>
}, <Pie
datalabels: { data={{
formatter: (value) => `${value}%`, labels: question.options.map(opt => opt.text),
color: '#000', datasets: [{
font: { weight: 400, size: 12 } data: question.options.map(opt => opt.percentage),
} backgroundColor: colorsForPie,
}, borderColor: '#fff',
animation: { animateRotate: true } borderWidth: 2
}} }]
/> }}
options={{
responsive: true,
plugins: {
legend: {
position: 'right',
labels: {
color: '#000000',
font: {
size: 12,
weight: 500
}
}
},
tooltip: {
callbacks: {
label: (ctx) => `${ctx.label}: ${ctx.raw}%`
}
},
datalabels: {
formatter: (value) => `${value}%`,
color: '#000',
font: { weight: 400, size: 12 }
}
},
animation: { animateRotate: true }
}}
/>
</div>
)}
</div> </div>
)} </div>
</div> </div>
</div> ))
)}
{surveyStats.totalParticipants === 0 ? <></> : (
<div className={styles.exportButtonWrapper}>
<button
ref={exportButtonRef}
className={styles.exportButtonContainer}
onClick={() => handleExportToExcel(survey.id)}
>
Экспорт в excel
</button>
{showExportPopup && (
<div className={styles.exportPopup}>
Файл скачивается
</div>
)}
</div> </div>
))} )}
</div>
<button className={styles.exportButtonContainer} onClick={() => handleExportToExcel(survey.id)}>Экспорт в excel</button>
</div>
); );
}; };

View file

@ -26,9 +26,50 @@
border-radius: 4px; border-radius: 4px;
} }
.buttonContainer {
position: relative;
display: inline-block;
width: 100%;
text-align: center;
}
.popup {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255, 255, 255, 0.9);
color: #333;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
white-space: nowrap;
animation: fadeInOut 2s ease-in-out;
z-index: 10;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
20% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
80% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
100% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
}
.copyButton { .copyButton {
padding: 10px 15px; padding: 10px 15px;
/*background-color: #4CAF50;*/
background-color: #3788D6; background-color: #3788D6;
color: white; color: white;
border: none; border: none;
@ -40,5 +81,5 @@
} }
.copyButton:hover { .copyButton:hover {
background-color: #45a049; background-color: rgb(14, 122, 221);
} }

View file

@ -1,8 +1,7 @@
import React from 'react'; import React, {useRef, useState} from 'react';
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import styles from "./SettingSurvey.module.css"; import styles from "./SettingSurvey.module.css";
import TimeEvent from "../TimeEvent/TimeEvent.tsx"; import TimeEvent from "../TimeEvent/TimeEvent.tsx";
import SaveButton from "../SaveButton/SaveButton.tsx";
import {ISurvey} from "../../api/SurveyApi.ts"; import {ISurvey} from "../../api/SurveyApi.ts";
import {useLocation, useOutletContext} from "react-router-dom"; import {useLocation, useOutletContext} from "react-router-dom";
import {useSurveyContext} from "../../context/SurveyContext.tsx"; import {useSurveyContext} from "../../context/SurveyContext.tsx";
@ -16,13 +15,17 @@ const SettingSurvey: React.FC = () => {
setSurvey: (survey: ISurvey) => void; setSurvey: (survey: ISurvey) => void;
}>(); }>();
const { tempSurvey, setTempSurvey } = useSurveyContext(); const { tempSurvey, setTempSurvey } = useSurveyContext();
const [showPopup, setShowPopup] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const handleCopyLink = () => { const handleCopyLink = () => {
if (!survey?.id) if (!survey?.id) return;
return;
const link = `${window.location.origin}/complete-survey/${survey.id}`; const link = `${window.location.origin}/complete-survey/${survey.id}`;
navigator.clipboard.writeText(link) navigator.clipboard.writeText(link)
.then(() => console.log('Copied!')) .then(() => {
setShowPopup(true);
setTimeout(() => setShowPopup(false), 2000);
})
.catch(error => console.error(`Не удалось скопировать ссылку: ${error}`)); .catch(error => console.error(`Не удалось скопировать ссылку: ${error}`));
} }
@ -50,8 +53,22 @@ const SettingSurvey: React.FC = () => {
<div className={styles.param}> <div className={styles.param}>
<h2>Параметры видимости</h2> <h2>Параметры видимости</h2>
</div> </div>
<SaveButton onClick={() => {}}/> {!isSettingCreatePage && (
{!isSettingCreatePage ? <button onClick={handleCopyLink} className={styles.copyButton}>Копировать ссылку</button> : ''} <div className={styles.buttonContainer}>
<button
ref={buttonRef}
onClick={handleCopyLink}
className={styles.copyButton}
>
Копировать ссылку
</button>
{showPopup && (
<div className={styles.popup}>
Ссылка скопирована
</div>
)}
</div>
)}
</div> </div>
) )
} }

View file

@ -235,12 +235,13 @@ export const SurveyPage: React.FC = () => {
surveyId: id, surveyId: id,
id: question.id, id: question.id,
title: question.text, title: question.text,
questionType: question.questionType // Убедитесь, что передается новый тип questionType: question.questionType
} as QuestionActionData & { id: number } } as QuestionActionData & { id: number }
}); });
} }
}); });
questions.forEach(question => { questions.forEach(question => {
question.answerVariants.forEach(answer => { question.answerVariants.forEach(answer => {
if (!answer.id) { if (!answer.id) {