getting results and an excel file

This commit is contained in:
Tatiana Nikolaeva 2025-06-09 16:22:52 +05:00
parent a75275c7de
commit 3c466a98d3
8 changed files with 214 additions and 42 deletions

View file

@ -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>

View file

@ -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'),
}) })

View 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;
}
}

View file

@ -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('Произошла ошибка при отправке ответов');
} }
}; };

View file

@ -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;
}

View file

@ -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>
); );
}; };

View file

@ -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;
} }

View file

@ -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) {