fix results page
This commit is contained in:
parent
3c466a98d3
commit
1e60984d64
9 changed files with 267 additions and 136 deletions
|
|
@ -11,6 +11,10 @@ export const handleUnauthorizedError = (error: unknown) => {
|
|||
window.location.href = '/login';
|
||||
console.log('Сессия истекла. Перенаправление на страницу входа.');
|
||||
}
|
||||
if (error instanceof Error && error.message.includes('Токен отсутствует')) {
|
||||
window.location.href = '/login';
|
||||
console.log('Сессия истекла. Перенаправление на страницу входа.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ export const updateQuestion = async (id: number, question: Partial<INewQuestion>
|
|||
questionType: question.questionType,
|
||||
}),
|
||||
})
|
||||
return await handleResponse(response)
|
||||
const result = await handleResponse(response);
|
||||
return result;
|
||||
}
|
||||
catch(error){
|
||||
handleUnauthorizedError(error);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import styles from './Account.module.css';
|
||||
import AccountImg from '../../assets/account.svg?react';
|
||||
import { getCurrentUser } from '../../api/AuthApi';
|
||||
import {handleUnauthorizedError} from "../../api/BaseApi.ts";
|
||||
|
||||
interface AccountProps {
|
||||
href: string;
|
||||
|
|
@ -17,6 +18,7 @@ const Account: React.FC<AccountProps> = ({ href }) => {
|
|||
const userData = await getCurrentUser();
|
||||
setUserName(`${userData.firstName} ${userData.lastName}`);
|
||||
} catch (error) {
|
||||
handleUnauthorizedError(error)
|
||||
console.error("Ошибка загрузки данных пользователя:", error);
|
||||
localStorage.removeItem("user");
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
|
|||
setTextQuestion(valueQuestion);
|
||||
}, [valueQuestion]);
|
||||
|
||||
useEffect(() => {
|
||||
setQuestionType(initialQuestionType);
|
||||
}, [initialQuestionType]);
|
||||
// useEffect(() => {
|
||||
// setQuestionType(initialQuestionType);
|
||||
// }, [initialQuestionType]);
|
||||
|
||||
const handleTypeChange = (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => {
|
||||
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 answerText = initialAnswerVariants[index].text;
|
||||
|
||||
|
|
@ -234,7 +222,13 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
|
|||
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
|
||||
</button>
|
||||
)}
|
||||
<TypeDropdown selectedType={questionType} onTypeChange={handleTypeChange}/>
|
||||
|
||||
<TypeDropdown
|
||||
selectedType={questionType}
|
||||
onTypeChange={(type) => {
|
||||
handleTypeChange(type);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{initialAnswerVariants.map((answer, index) => (
|
||||
|
|
|
|||
|
|
@ -147,7 +147,6 @@
|
|||
|
||||
.exportButtonContainer {
|
||||
padding: 10px 15px;
|
||||
/*background-color: #4CAF50;*/
|
||||
background-color: #3788D6;
|
||||
color: white;
|
||||
border: none;
|
||||
|
|
@ -159,5 +158,52 @@
|
|||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import Group from '../../assets/gmail_groups.svg?react';
|
|||
import Send from '../../assets/send.svg?react';
|
||||
import {getAllCompletions} from "../../api/CompletionApi.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 {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts";
|
||||
import {getResultsFile} from "../../api/ExportResultApi.ts";
|
||||
|
|
@ -52,10 +52,18 @@ export const Results = () => {
|
|||
});
|
||||
|
||||
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) => {
|
||||
await getResultsFile(id)
|
||||
try {
|
||||
await getResultsFile(id);
|
||||
setShowExportPopup(true);
|
||||
setTimeout(() => setShowExportPopup(false), 2000);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при экспорте:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -65,7 +73,6 @@ export const Results = () => {
|
|||
setLocalSurvey(surveyData);
|
||||
|
||||
const questionsList = await getListQuestions(survey.id);
|
||||
// setQuestions(questionsList);
|
||||
|
||||
const questionsWithVariants = await Promise.all(
|
||||
questionsList.map(async (question) => {
|
||||
|
|
@ -205,116 +212,134 @@ export const Results = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{surveyStats.questions.map((question, index) => (
|
||||
<div key={index} className={styles.questionContainer}>
|
||||
<div className={styles.questionContent}>
|
||||
<div className={styles.textContainer}>
|
||||
<h3>{question.questionText}</h3>
|
||||
<p className={styles.answerCount}>Ответов: {question.totalAnswers}</p>
|
||||
</div>
|
||||
{surveyStats.totalParticipants === 0 ? (
|
||||
<p className={styles.info}>Нет данных о прохождении опроса</p>
|
||||
) : (
|
||||
surveyStats.questions.map((question, index) => (
|
||||
<div key={index} className={styles.questionContainer}>
|
||||
<div className={styles.questionContent}>
|
||||
<div className={styles.textContainer}>
|
||||
<h3>{question.questionText}</h3>
|
||||
<p className={styles.answerCount}>Ответов: {question.totalAnswers}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.chartContainer}>
|
||||
{question.isMultipleChoice ? (
|
||||
<div className={styles.barContainer}>
|
||||
<Bar
|
||||
data={{
|
||||
labels: question.options.map(opt => opt.text),
|
||||
datasets: [{
|
||||
label: '% выбравших',
|
||||
data: question.options.map(opt => opt.percentage),
|
||||
backgroundColor: colorsForBar,
|
||||
borderColor: colorsForBar,
|
||||
borderWidth: 2,
|
||||
borderRadius: 8,
|
||||
borderSkipped: false,
|
||||
}]
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: { enabled: true },
|
||||
datalabels: { display: false },
|
||||
annotation: {
|
||||
annotations: question.options.map((opt, i) => ({
|
||||
type: 'label',
|
||||
xValue: i,
|
||||
yValue: opt.percentage + 5,
|
||||
content: `${opt.percentage}%`,
|
||||
font: { size: 1, weight: 400 },
|
||||
color: '#000'
|
||||
}))
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: { callback: (val) => `${val}%` }
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#000000',
|
||||
font: {
|
||||
size: 12,
|
||||
weight: 400
|
||||
<div className={styles.chartContainer}>
|
||||
{question.isMultipleChoice ? (
|
||||
<div className={styles.barContainer}>
|
||||
<Bar
|
||||
data={{
|
||||
labels: question.options.map(opt => opt.text),
|
||||
datasets: [{
|
||||
label: '% выбравших',
|
||||
data: question.options.map(opt => opt.percentage),
|
||||
backgroundColor: colorsForBar,
|
||||
borderColor: colorsForBar,
|
||||
borderWidth: 2,
|
||||
borderRadius: 8,
|
||||
borderSkipped: false,
|
||||
}]
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: { enabled: true },
|
||||
datalabels: { display: false },
|
||||
annotation: {
|
||||
annotations: question.options.map((opt, i) => ({
|
||||
type: 'label',
|
||||
xValue: i,
|
||||
yValue: opt.percentage + 5,
|
||||
content: `${opt.percentage}%`,
|
||||
font: { size: 1, weight: 400 },
|
||||
color: '#000'
|
||||
}))
|
||||
}
|
||||
},
|
||||
grid: { display: false }
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.pieContainer}>
|
||||
<Pie
|
||||
data={{
|
||||
labels: question.options.map(opt => opt.text),
|
||||
datasets: [{
|
||||
data: question.options.map(opt => opt.percentage),
|
||||
backgroundColor: colorsForPie,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
color: '#000000',
|
||||
font: {
|
||||
size: 12,
|
||||
weight: 500
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: { callback: (val) => `${val}%` }
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#000000',
|
||||
font: {
|
||||
size: 12,
|
||||
weight: 400
|
||||
}
|
||||
},
|
||||
grid: { display: false }
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx) => `${ctx.label}: ${ctx.raw}%`
|
||||
}
|
||||
},
|
||||
datalabels: {
|
||||
formatter: (value) => `${value}%`,
|
||||
color: '#000',
|
||||
font: { weight: 400, size: 12 }
|
||||
}
|
||||
},
|
||||
animation: { animateRotate: true }
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.pieContainer}>
|
||||
<Pie
|
||||
data={{
|
||||
labels: question.options.map(opt => opt.text),
|
||||
datasets: [{
|
||||
data: question.options.map(opt => opt.percentage),
|
||||
backgroundColor: colorsForPie,
|
||||
borderColor: '#fff',
|
||||
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>
|
||||
))
|
||||
)}
|
||||
{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>
|
||||
))}
|
||||
|
||||
<button className={styles.exportButtonContainer} onClick={() => handleExportToExcel(survey.id)}>Экспорт в excel</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -26,9 +26,50 @@
|
|||
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 {
|
||||
padding: 10px 15px;
|
||||
/*background-color: #4CAF50;*/
|
||||
background-color: #3788D6;
|
||||
color: white;
|
||||
border: none;
|
||||
|
|
@ -40,5 +81,5 @@
|
|||
}
|
||||
|
||||
.copyButton:hover {
|
||||
background-color: #45a049;
|
||||
background-color: rgb(14, 122, 221);
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import React, {useRef, useState} from 'react';
|
||||
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
||||
import styles from "./SettingSurvey.module.css";
|
||||
import TimeEvent from "../TimeEvent/TimeEvent.tsx";
|
||||
import SaveButton from "../SaveButton/SaveButton.tsx";
|
||||
import {ISurvey} from "../../api/SurveyApi.ts";
|
||||
import {useLocation, useOutletContext} from "react-router-dom";
|
||||
import {useSurveyContext} from "../../context/SurveyContext.tsx";
|
||||
|
|
@ -16,13 +15,17 @@ const SettingSurvey: React.FC = () => {
|
|||
setSurvey: (survey: ISurvey) => void;
|
||||
}>();
|
||||
const { tempSurvey, setTempSurvey } = useSurveyContext();
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
if (!survey?.id)
|
||||
return;
|
||||
if (!survey?.id) return;
|
||||
const link = `${window.location.origin}/complete-survey/${survey.id}`;
|
||||
navigator.clipboard.writeText(link)
|
||||
.then(() => console.log('Copied!'))
|
||||
.then(() => {
|
||||
setShowPopup(true);
|
||||
setTimeout(() => setShowPopup(false), 2000);
|
||||
})
|
||||
.catch(error => console.error(`Не удалось скопировать ссылку: ${error}`));
|
||||
}
|
||||
|
||||
|
|
@ -50,8 +53,22 @@ const SettingSurvey: React.FC = () => {
|
|||
<div className={styles.param}>
|
||||
<h2>Параметры видимости</h2>
|
||||
</div>
|
||||
<SaveButton onClick={() => {}}/>
|
||||
{!isSettingCreatePage ? <button onClick={handleCopyLink} className={styles.copyButton}>Копировать ссылку</button> : ''}
|
||||
{!isSettingCreatePage && (
|
||||
<div className={styles.buttonContainer}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={handleCopyLink}
|
||||
className={styles.copyButton}
|
||||
>
|
||||
Копировать ссылку
|
||||
</button>
|
||||
{showPopup && (
|
||||
<div className={styles.popup}>
|
||||
Ссылка скопирована
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,12 +235,13 @@ export const SurveyPage: React.FC = () => {
|
|||
surveyId: id,
|
||||
id: question.id,
|
||||
title: question.text,
|
||||
questionType: question.questionType // Убедитесь, что передается новый тип
|
||||
questionType: question.questionType
|
||||
} as QuestionActionData & { id: number }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
questions.forEach(question => {
|
||||
question.answerVariants.forEach(answer => {
|
||||
if (!answer.id) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue