diff --git a/SurveyFrontend/src/App.tsx b/SurveyFrontend/src/App.tsx index 9532ce7..27ff95c 100644 --- a/SurveyFrontend/src/App.tsx +++ b/SurveyFrontend/src/App.tsx @@ -29,7 +29,7 @@ const App = () => { }> } /> - } /> + } /> } /> diff --git a/SurveyFrontend/src/api/AnswerApi.ts b/SurveyFrontend/src/api/AnswerApi.ts index 26bbec5..d55ea44 100644 --- a/SurveyFrontend/src/api/AnswerApi.ts +++ b/SurveyFrontend/src/api/AnswerApi.ts @@ -26,7 +26,7 @@ export const getCompletionsAnswer = async (id: number) => { } try{ - const response = await fetch(`${BASE_URL}/questions/${id}/answers`, { + const response = await fetch(`${BASE_URL}/completions/${id}/answers`, { ...createRequestConfig('GET'), }) diff --git a/SurveyFrontend/src/api/ExportResultApi.ts b/SurveyFrontend/src/api/ExportResultApi.ts new file mode 100644 index 0000000..972a7fd --- /dev/null +++ b/SurveyFrontend/src/api/ExportResultApi.ts @@ -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; + } +} \ No newline at end of file diff --git a/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx b/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx index fc2141b..2825a3c 100644 --- a/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx +++ b/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx @@ -87,10 +87,9 @@ export const CompletingSurvey = () => { answers: selectedAnswers }); - navigate('/surveys'); + navigate('/my-surveys'); } catch (error) { console.error('Ошибка при отправке ответов:', error); - alert('Произошла ошибка при отправке ответов'); } }; diff --git a/SurveyFrontend/src/components/Results/Results.module.css b/SurveyFrontend/src/components/Results/Results.module.css index 4ca8caa..6de347c 100644 --- a/SurveyFrontend/src/components/Results/Results.module.css +++ b/SurveyFrontend/src/components/Results/Results.module.css @@ -133,7 +133,7 @@ } .pieContainer { - width: 100%; + width: 70%; position: relative; } @@ -144,3 +144,20 @@ align-items: center; 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; +} \ No newline at end of file diff --git a/SurveyFrontend/src/components/Results/Results.tsx b/SurveyFrontend/src/components/Results/Results.tsx index ea17ca7..4a02574 100644 --- a/SurveyFrontend/src/components/Results/Results.tsx +++ b/SurveyFrontend/src/components/Results/Results.tsx @@ -3,12 +3,17 @@ import styles from './Results.module.css'; import {Bar, Pie} from 'react-chartjs-2'; import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js'; 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 annotationPlugin from 'chartjs-plugin-annotation'; 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 {getListQuestions} from "../../api/QuestionApi.ts"; +import {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts"; +import {getResultsFile} from "../../api/ExportResultApi.ts"; ChartJS.register( ArcElement, Tooltip, Legend, @@ -21,47 +26,147 @@ interface QuestionStats { options: { text: string; percentage: number; + id: number; }[]; - isMultipleChoice?: boolean; + isMultipleChoice: boolean; +} + +interface IAnswer { + questionId: number; + completionId: number; + answerText: string; + optionId?: number; } export const Results = () => { - const { survey, setSurvey } = useOutletContext<{ + const { survey: initialSurvey, setSurvey } = useOutletContext<{ survey: ISurvey; setSurvey: (survey: ISurvey) => void; }>(); - - const surveyStats = { - totalParticipants: 100, - completionPercentage: 80, + const [surveyStats, setSurveyStats] = useState({ + totalParticipants: 0, + completionPercentage: 0, status: 'Активен', - questions: [ - { - questionText: "Вопрос 1", - totalAnswers: 80, - options: [ - { text: "Вариант 1", percentage: 46 }, - { text: "Вариант 2", percentage: 15 }, - { text: "Вариант 3", percentage: 39 } - ], - 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[] + questions: [] as QuestionStats[] + }); + + const [survey, setLocalSurvey] = useState(initialSurvey); + // const [questions, setQuestions] = useState([]); + + const handleExportToExcel = async (id: number) => { + await getResultsFile(id) }; - // Цветовая палитра + 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); + + 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 colorsForBar = ['#8979FF']; @@ -70,8 +175,14 @@ export const Results = () => { setSurvey({ ...survey, description: value })} - setTitleSurvey={(value) => setSurvey({ ...survey, title: value })} + setDescriptionSurvey={(value) => { + setSurvey({ ...survey, description: value }); + setLocalSurvey({ ...survey, description: value }); + }} + setTitleSurvey={(value) => { + setSurvey({ ...survey, title: value }); + setLocalSurvey({ ...survey, title: value }); + }} /> @@ -202,6 +313,8 @@ export const Results = () => { ))} + + handleExportToExcel(survey.id)}>Экспорт в excel ); }; \ No newline at end of file diff --git a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css index 89e8e06..6f229cd 100644 --- a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css +++ b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css @@ -49,8 +49,8 @@ } .textareaTitle { - margin-top: 15px; - margin-bottom: -2px; + margin-top: 14px; + margin-bottom: -1px; font-size: 26px; font-weight: 600; text-align: center; @@ -64,6 +64,7 @@ text-align: center; line-height: 1.4; min-height: 24px; + margin-top: -2px; margin-bottom: -3px; } diff --git a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx index 3a6e3b5..cf35990 100644 --- a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx +++ b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx @@ -121,7 +121,7 @@ const SurveyInfo: React.FC = ({titleSurvey, setDescriptionSurve if (isReadOnly) { return descriptionSurvey ? ( {descriptionSurvey} - ) : 'Описание'; + ) : ''; } if (descriptionSurvey && !showDescriptionField) {
{descriptionSurvey}