getting results and an excel file
This commit is contained in:
parent
a75275c7de
commit
3c466a98d3
8 changed files with 214 additions and 42 deletions
|
|
@ -29,7 +29,7 @@ const App = () => {
|
||||||
|
|
||||||
<Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}>
|
<Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}>
|
||||||
<Route path="questions" element={<SurveyPage />} />
|
<Route path="questions" element={<SurveyPage />} />
|
||||||
<Route path="settings" element={<SettingSurvey />} />
|
<Route path="settings" element={<SurveyProvider><SettingSurvey /></SurveyProvider>} />
|
||||||
<Route path="results" element={<Results />} />
|
<Route path="results" element={<Results />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export const getCompletionsAnswer = async (id: number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
try{
|
||||||
const response = await fetch(`${BASE_URL}/questions/${id}/answers`, {
|
const response = await fetch(`${BASE_URL}/completions/${id}/answers`, {
|
||||||
...createRequestConfig('GET'),
|
...createRequestConfig('GET'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
42
SurveyFrontend/src/api/ExportResultApi.ts
Normal file
42
SurveyFrontend/src/api/ExportResultApi.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {BASE_URL, createRequestConfig, handleUnauthorizedError} from "./BaseApi.ts";
|
||||||
|
|
||||||
|
export const getResultsFile = async (surveyId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BASE_URL}/export/excel/${surveyId}`, {
|
||||||
|
...createRequestConfig('GET'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка при получении файла');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
|
||||||
|
const contentDisposition = response.headers.get('content-disposition');
|
||||||
|
let fileName = `survey_results_${surveyId}.xlsx`;
|
||||||
|
|
||||||
|
if (contentDisposition) {
|
||||||
|
const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/);
|
||||||
|
if (fileNameMatch && fileNameMatch[1]) {
|
||||||
|
fileName = fileNameMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
handleUnauthorizedError(error);
|
||||||
|
console.error(`Error when receiving survey results: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,10 +87,9 @@ export const CompletingSurvey = () => {
|
||||||
answers: selectedAnswers
|
answers: selectedAnswers
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate('/surveys');
|
navigate('/my-surveys');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при отправке ответов:', error);
|
console.error('Ошибка при отправке ответов:', error);
|
||||||
alert('Произошла ошибка при отправке ответов');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pieContainer {
|
.pieContainer {
|
||||||
width: 100%;
|
width: 70%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,3 +144,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 150px;
|
padding-right: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.exportButtonContainer {
|
||||||
|
padding: 10px 15px;
|
||||||
|
/*background-color: #4CAF50;*/
|
||||||
|
background-color: #3788D6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
display: block;
|
||||||
|
margin: 30px auto 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exportButtonContainer:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,17 @@ import styles from './Results.module.css';
|
||||||
import {Bar, Pie} from 'react-chartjs-2';
|
import {Bar, Pie} from 'react-chartjs-2';
|
||||||
import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js';
|
import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js';
|
||||||
import {useOutletContext} from "react-router-dom";
|
import {useOutletContext} from "react-router-dom";
|
||||||
import {ISurvey} from "../../api/SurveyApi.ts";
|
import {ISurvey, getSurveyById} from "../../api/SurveyApi.ts";
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import Group from '../../assets/gmail_groups.svg?react';
|
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 {getAnswer} from "../../api/AnswerApi.ts";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {getListQuestions} from "../../api/QuestionApi.ts";
|
||||||
|
import {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts";
|
||||||
|
import {getResultsFile} from "../../api/ExportResultApi.ts";
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
ArcElement, Tooltip, Legend,
|
ArcElement, Tooltip, Legend,
|
||||||
|
|
@ -21,47 +26,147 @@ interface QuestionStats {
|
||||||
options: {
|
options: {
|
||||||
text: string;
|
text: string;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
|
id: number;
|
||||||
}[];
|
}[];
|
||||||
isMultipleChoice?: boolean;
|
isMultipleChoice: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAnswer {
|
||||||
|
questionId: number;
|
||||||
|
completionId: number;
|
||||||
|
answerText: string;
|
||||||
|
optionId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Results = () => {
|
export const Results = () => {
|
||||||
const { survey, setSurvey } = useOutletContext<{
|
const { survey: initialSurvey, setSurvey } = useOutletContext<{
|
||||||
survey: ISurvey;
|
survey: ISurvey;
|
||||||
setSurvey: (survey: ISurvey) => void;
|
setSurvey: (survey: ISurvey) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const [surveyStats, setSurveyStats] = useState({
|
||||||
const surveyStats = {
|
totalParticipants: 0,
|
||||||
totalParticipants: 100,
|
completionPercentage: 0,
|
||||||
completionPercentage: 80,
|
|
||||||
status: 'Активен',
|
status: 'Активен',
|
||||||
questions: [
|
questions: [] as QuestionStats[]
|
||||||
{
|
});
|
||||||
questionText: "Вопрос 1",
|
|
||||||
totalAnswers: 80,
|
const [survey, setLocalSurvey] = useState<ISurvey>(initialSurvey);
|
||||||
options: [
|
// const [questions, setQuestions] = useState<IQuestion[]>([]);
|
||||||
{ text: "Вариант 1", percentage: 46 },
|
|
||||||
{ text: "Вариант 2", percentage: 15 },
|
const handleExportToExcel = async (id: number) => {
|
||||||
{ text: "Вариант 3", percentage: 39 }
|
await getResultsFile(id)
|
||||||
],
|
|
||||||
isMultipleChoice: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
questionText: "Вопрос 2",
|
|
||||||
totalAnswers: 100,
|
|
||||||
options: [
|
|
||||||
{ text: "Вариант 1", percentage: 50 },
|
|
||||||
{ text: "Вариант 2", percentage: 20 },
|
|
||||||
{ text: "Вариант 3", percentage: 100 },
|
|
||||||
{ text: "Вариант 4", percentage: 80 }
|
|
||||||
],
|
|
||||||
isMultipleChoice: true
|
|
||||||
}
|
|
||||||
] as QuestionStats[]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Цветовая палитра
|
useEffect(() => {
|
||||||
|
const fetchSurveyData = async () => {
|
||||||
|
try {
|
||||||
|
const surveyData = await getSurveyById(survey.id);
|
||||||
|
setLocalSurvey(surveyData);
|
||||||
|
|
||||||
|
const questionsList = await getListQuestions(survey.id);
|
||||||
|
// setQuestions(questionsList);
|
||||||
|
|
||||||
|
const questionsWithVariants = await Promise.all(
|
||||||
|
questionsList.map(async (question) => {
|
||||||
|
const variants = await getAnswerVariants(survey.id, question.id);
|
||||||
|
return {
|
||||||
|
...question,
|
||||||
|
options: variants,
|
||||||
|
isMultipleChoice: question.questionType !== 'SingleAnswerQuestion'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const completions = await getAllCompletions(survey.id);
|
||||||
|
const totalParticipants = completions.length;
|
||||||
|
|
||||||
|
const questionsWithAnswers = await Promise.all(
|
||||||
|
questionsWithVariants.map(async (question) => {
|
||||||
|
const answers = await getAnswer(question.id);
|
||||||
|
return {
|
||||||
|
...question,
|
||||||
|
answers: answers
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const questionsStats = questionsWithAnswers.map(question => {
|
||||||
|
const questionAnswers = question.answers as IAnswer[];
|
||||||
|
|
||||||
|
let uniqueAnswers = questionAnswers;
|
||||||
|
if (question.isMultipleChoice) {
|
||||||
|
const groupedByCompletion = questionAnswers.reduce((acc, answer) => {
|
||||||
|
if (!acc[answer.completionId]) {
|
||||||
|
acc[answer.completionId] = [];
|
||||||
|
}
|
||||||
|
acc[answer.completionId].push(answer);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, IAnswer[]>);
|
||||||
|
|
||||||
|
uniqueAnswers = Object.values(groupedByCompletion).map(group => ({
|
||||||
|
...group[0],
|
||||||
|
answerText: group.map(a => a.answerText).join("; ")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsStats = question.options.map((option: IAnswerVariant) => {
|
||||||
|
const count = uniqueAnswers.filter(answer =>
|
||||||
|
question.isMultipleChoice
|
||||||
|
? answer.answerText.includes(option.text)
|
||||||
|
: answer.answerText === option.text
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const percentage = totalParticipants > 0
|
||||||
|
? Math.round((count / totalParticipants) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: option.text,
|
||||||
|
percentage: percentage,
|
||||||
|
id: option.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
questionText: question.title,
|
||||||
|
totalAnswers: uniqueAnswers.length,
|
||||||
|
options: optionsStats,
|
||||||
|
isMultipleChoice: question.isMultipleChoice
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalAnswersCount = questionsWithAnswers.reduce((sum, question) => {
|
||||||
|
if (question.isMultipleChoice) {
|
||||||
|
const uniqueCompletions = new Set(
|
||||||
|
question.answers.map((answer : IAnswer) => answer.completionId)
|
||||||
|
);
|
||||||
|
return sum + uniqueCompletions.size;
|
||||||
|
} else {
|
||||||
|
return sum + question.answers.length;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const maxPossibleAnswers = questionsWithAnswers.length * totalParticipants;
|
||||||
|
const completionPercentage = totalParticipants > 0 && maxPossibleAnswers > 0
|
||||||
|
? Math.round((totalAnswersCount / maxPossibleAnswers) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
setSurveyStats({
|
||||||
|
totalParticipants,
|
||||||
|
completionPercentage,
|
||||||
|
status: 'Активен',
|
||||||
|
questions: questionsStats
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching survey data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSurveyData();
|
||||||
|
}, [survey.id]);
|
||||||
|
|
||||||
const colorsForPie = ['#67C587', '#C9EAD4', '#EAF6ED'];
|
const colorsForPie = ['#67C587', '#C9EAD4', '#EAF6ED'];
|
||||||
const colorsForBar = ['#8979FF'];
|
const colorsForBar = ['#8979FF'];
|
||||||
|
|
||||||
|
|
@ -70,8 +175,14 @@ export const Results = () => {
|
||||||
<SurveyInfo
|
<SurveyInfo
|
||||||
titleSurvey={survey.title}
|
titleSurvey={survey.title}
|
||||||
descriptionSurvey={survey.description}
|
descriptionSurvey={survey.description}
|
||||||
setDescriptionSurvey={(value) => setSurvey({ ...survey, description: value })}
|
setDescriptionSurvey={(value) => {
|
||||||
setTitleSurvey={(value) => setSurvey({ ...survey, title: value })}
|
setSurvey({ ...survey, description: value });
|
||||||
|
setLocalSurvey({ ...survey, description: value });
|
||||||
|
}}
|
||||||
|
setTitleSurvey={(value) => {
|
||||||
|
setSurvey({ ...survey, title: value });
|
||||||
|
setLocalSurvey({ ...survey, title: value });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={styles.statsContainer}>
|
<div className={styles.statsContainer}>
|
||||||
<div className={`${styles.statItem} ${styles.countAnswer}`}>
|
<div className={`${styles.statItem} ${styles.countAnswer}`}>
|
||||||
|
|
@ -202,6 +313,8 @@ export const Results = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<button className={styles.exportButtonContainer} onClick={() => handleExportToExcel(survey.id)}>Экспорт в excel</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -49,8 +49,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.textareaTitle {
|
.textareaTitle {
|
||||||
margin-top: 15px;
|
margin-top: 14px;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -1px;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
|
margin-top: -2px;
|
||||||
margin-bottom: -3px;
|
margin-bottom: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return descriptionSurvey ? (
|
return descriptionSurvey ? (
|
||||||
<p className={styles.desc}>{descriptionSurvey}</p>
|
<p className={styles.desc}>{descriptionSurvey}</p>
|
||||||
) : 'Описание';
|
) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (descriptionSurvey && !showDescriptionField) {
|
if (descriptionSurvey && !showDescriptionField) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue