320 lines
No EOL
15 KiB
TypeScript
320 lines
No EOL
15 KiB
TypeScript
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>
|
||
);
|
||
}; |