survey-webapp/SurveyFrontend/src/components/Results/Results.tsx
2025-06-09 16:22:52 +05:00

320 lines
No EOL
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
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, 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,
CategoryScale, LinearScale, BarElement, Title, ChartDataLabels, annotationPlugin
);
interface QuestionStats {
questionText: string;
totalAnswers: number;
options: {
text: string;
percentage: number;
id: number;
}[];
isMultipleChoice: boolean;
}
interface IAnswer {
questionId: number;
completionId: number;
answerText: string;
optionId?: number;
}
export const Results = () => {
const { survey: initialSurvey, setSurvey } = useOutletContext<{
survey: ISurvey;
setSurvey: (survey: ISurvey) => void;
}>();
const [surveyStats, setSurveyStats] = useState({
totalParticipants: 0,
completionPercentage: 0,
status: 'Активен',
questions: [] as QuestionStats[]
});
const [survey, setLocalSurvey] = useState<ISurvey>(initialSurvey);
// const [questions, setQuestions] = useState<IQuestion[]>([]);
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<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 colorsForBar = ['#8979FF'];
return (
<div className={styles.results}>
<SurveyInfo
titleSurvey={survey.title}
descriptionSurvey={survey.description}
setDescriptionSurvey={(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.statItem} ${styles.countAnswer}`}>
<h3>Количество ответов</h3>
<div className={styles.result}>
<p>{surveyStats.totalParticipants}</p>
<Group className={styles.imgGroup}/>
</div>
</div>
<div className={`${styles.statItem} ${styles.completion_percentage}`}>
<h3>Процент завершения</h3>
<div className={styles.result}>
<p>{surveyStats.completionPercentage}%</p>
<Send className={styles.imgSend}/>
</div>
</div>
<div className={`${styles.statItem} ${styles.status}`}>
<h3>Статус опроса</h3>
<p>{surveyStats.status}</p>
</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>
<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
}
},
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
}
}
},
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>
))}
<button className={styles.exportButtonContainer} onClick={() => handleExportToExcel(survey.id)}>Экспорт в excel</button>
</div>
);
};