Merge branch 'unstable' into 'main'

Million fixes

See merge request internship-2025/survey-webapp/survey-webapp!31
This commit is contained in:
Вячеслав 2025-06-10 08:44:57 +00:00
commit 70f287a0b6
12 changed files with 289 additions and 138 deletions

View file

@ -71,7 +71,8 @@ public class QuestionController : ControllerBase
var question = QuestionMapper.QuestionUpdateToModel(dto, id);
await _questionService.UpdateQuestionAsync(question);
var updatedQuestion = await _questionService.GetQuestionByIdAsync(id);
return Ok(updatedQuestion);
var result = QuestionMapper.ModelToQuestionDto(updatedQuestion);
return Ok(result);
}
/// <summary>

View file

@ -69,16 +69,22 @@ public static class QuestionMapper
{
Id = questionId,
Title = dto.Title,
Discriminator = "TextQuestion",
AnswerVariants = []
},
"singleanswerquestion" => new SingleAnswerQuestion
{
Id = questionId,
Title = dto.Title,
Discriminator = "SingleAnswerQuestion",
AnswerVariants = []
},
"multipleanswerquestion" => new MultipleAnswerQuestion
{
Id = questionId,
Title = dto.Title,
Discriminator = "MultipleAnswerQuestion",
AnswerVariants = []
},
_ => throw new BadRequestException("Unknown question type")
};

View file

@ -41,8 +41,21 @@ public class QuestionService : IQuestionService
question.SurveyId = questionBase.SurveyId;
// Если изменился тип вопроса (Discriminator), используем новый тип
if (questionBase.Discriminator != question.Discriminator)
{
// Удаляем старый вопрос
await _questionRepository.DeleteAsync(questionBase.Id);
// Добавляем новый с тем же ID
await _questionRepository.AddAsync(question);
}
else
{
// Если тип не меняется, просто обновляем
await _questionRepository.UpdateAsync(question);
}
}
public async Task DeleteQuestionAsync(int id)
{

View file

@ -11,6 +11,10 @@ export const handleUnauthorizedError = (error: unknown) => {
window.location.href = '/login';
console.log('Сессия истекла. Перенаправление на страницу входа.');
}
if (error instanceof Error && error.message.includes('Токен отсутствует')) {
window.location.href = '/login';
console.log('Сессия истекла. Перенаправление на страницу входа.');
}
};
/**

View file

@ -58,7 +58,8 @@ export const updateQuestion = async (id: number, question: Partial<INewQuestion>
questionType: question.questionType,
}),
})
return await handleResponse(response)
const result = await handleResponse(response);
return result;
}
catch(error){
handleUnauthorizedError(error);

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import styles from './Account.module.css';
import AccountImg from '../../assets/account.svg?react';
import { getCurrentUser } from '../../api/AuthApi';
import {handleUnauthorizedError} from "../../api/BaseApi.ts";
interface AccountProps {
href: string;
@ -17,6 +18,7 @@ const Account: React.FC<AccountProps> = ({ href }) => {
const userData = await getCurrentUser();
setUserName(`${userData.firstName} ${userData.lastName}`);
} catch (error) {
handleUnauthorizedError(error)
console.error("Ошибка загрузки данных пользователя:", error);
localStorage.removeItem("user");
} finally {

View file

@ -53,9 +53,9 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
setTextQuestion(valueQuestion);
}, [valueQuestion]);
useEffect(() => {
setQuestionType(initialQuestionType);
}, [initialQuestionType]);
// useEffect(() => {
// setQuestionType(initialQuestionType);
// }, [initialQuestionType]);
const handleTypeChange = (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => {
setQuestionType(type);
@ -167,18 +167,6 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
}
};
// const toggleSelect = (index: number) => {
// if (initialQuestionType === 'SingleAnswerQuestion') {
// setSelectedAnswers([index]);
// } else {
// setSelectedAnswers(prev =>
// prev.includes(index)
// ? prev.filter(i => i !== index)
// : [...prev, index]
// );
// }
// };
const toggleSelect = (index: number) => {
const answerText = initialAnswerVariants[index].text;
@ -234,7 +222,13 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
</button>
)}
<TypeDropdown selectedType={questionType} onTypeChange={handleTypeChange}/>
<TypeDropdown
selectedType={questionType}
onTypeChange={(type) => {
handleTypeChange(type);
}}
/>
</div>
{initialAnswerVariants.map((answer, index) => (

View file

@ -147,7 +147,6 @@
.exportButtonContainer {
padding: 10px 15px;
/*background-color: #4CAF50;*/
background-color: #3788D6;
color: white;
border: none;
@ -159,5 +158,52 @@
}
.exportButtonContainer:hover {
background-color: #45a049;
background-color: rgb(14, 122, 221);
}
.info{
margin-top: 60px;
text-align: center;
}
.exportButtonWrapper {
position: relative;
display: inline-block;
width: 100%;
text-align: center;
}
.exportPopup {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255, 255, 255, 0.9);
color: #333;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
white-space: nowrap;
animation: fadeInOut 2s ease-in-out;
z-index: 10;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
20% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
80% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
100% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
}

View file

@ -10,7 +10,7 @@ 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 {useEffect, useRef, useState} from "react";
import {getListQuestions} from "../../api/QuestionApi.ts";
import {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts";
import {getResultsFile} from "../../api/ExportResultApi.ts";
@ -52,10 +52,18 @@ export const Results = () => {
});
const [survey, setLocalSurvey] = useState<ISurvey>(initialSurvey);
// const [questions, setQuestions] = useState<IQuestion[]>([]);
const [showExportPopup, setShowExportPopup] = useState(false);
const exportButtonRef = useRef<HTMLButtonElement>(null);
const handleExportToExcel = async (id: number) => {
await getResultsFile(id)
try {
await getResultsFile(id);
setShowExportPopup(true);
setTimeout(() => setShowExportPopup(false), 2000);
} catch (error) {
console.error('Ошибка при экспорте:', error);
}
};
useEffect(() => {
@ -65,7 +73,6 @@ export const Results = () => {
setLocalSurvey(surveyData);
const questionsList = await getListQuestions(survey.id);
// setQuestions(questionsList);
const questionsWithVariants = await Promise.all(
questionsList.map(async (question) => {
@ -205,7 +212,10 @@ export const Results = () => {
</div>
</div>
{surveyStats.questions.map((question, index) => (
{surveyStats.totalParticipants === 0 ? (
<p className={styles.info}>Нет данных о прохождении опроса</p>
) : (
surveyStats.questions.map((question, index) => (
<div key={index} className={styles.questionContainer}>
<div className={styles.questionContent}>
<div className={styles.textContainer}>
@ -312,9 +322,24 @@ export const Results = () => {
</div>
</div>
</div>
))}
<button className={styles.exportButtonContainer} onClick={() => handleExportToExcel(survey.id)}>Экспорт в excel</button>
))
)}
{surveyStats.totalParticipants === 0 ? <></> : (
<div className={styles.exportButtonWrapper}>
<button
ref={exportButtonRef}
className={styles.exportButtonContainer}
onClick={() => handleExportToExcel(survey.id)}
>
Экспорт в excel
</button>
{showExportPopup && (
<div className={styles.exportPopup}>
Файл скачивается
</div>
)}
</div>
)}
</div>
);
};

View file

@ -26,9 +26,50 @@
border-radius: 4px;
}
.buttonContainer {
position: relative;
display: inline-block;
width: 100%;
text-align: center;
}
.popup {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255, 255, 255, 0.9);
color: #333;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
white-space: nowrap;
animation: fadeInOut 2s ease-in-out;
z-index: 10;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
20% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
80% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
100% {
opacity: 0;
transform: translateX(-50%) translateY(5px);
}
}
.copyButton {
padding: 10px 15px;
/*background-color: #4CAF50;*/
background-color: #3788D6;
color: white;
border: none;
@ -40,5 +81,5 @@
}
.copyButton:hover {
background-color: #45a049;
background-color: rgb(14, 122, 221);
}

View file

@ -1,8 +1,7 @@
import React from 'react';
import React, {useRef, useState} from 'react';
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import styles from "./SettingSurvey.module.css";
import TimeEvent from "../TimeEvent/TimeEvent.tsx";
import SaveButton from "../SaveButton/SaveButton.tsx";
import {ISurvey} from "../../api/SurveyApi.ts";
import {useLocation, useOutletContext} from "react-router-dom";
import {useSurveyContext} from "../../context/SurveyContext.tsx";
@ -16,13 +15,17 @@ const SettingSurvey: React.FC = () => {
setSurvey: (survey: ISurvey) => void;
}>();
const { tempSurvey, setTempSurvey } = useSurveyContext();
const [showPopup, setShowPopup] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const handleCopyLink = () => {
if (!survey?.id)
return;
if (!survey?.id) return;
const link = `${window.location.origin}/complete-survey/${survey.id}`;
navigator.clipboard.writeText(link)
.then(() => console.log('Copied!'))
.then(() => {
setShowPopup(true);
setTimeout(() => setShowPopup(false), 2000);
})
.catch(error => console.error(`Не удалось скопировать ссылку: ${error}`));
}
@ -50,8 +53,22 @@ const SettingSurvey: React.FC = () => {
<div className={styles.param}>
<h2>Параметры видимости</h2>
</div>
<SaveButton onClick={() => {}}/>
{!isSettingCreatePage ? <button onClick={handleCopyLink} className={styles.copyButton}>Копировать ссылку</button> : ''}
{!isSettingCreatePage && (
<div className={styles.buttonContainer}>
<button
ref={buttonRef}
onClick={handleCopyLink}
className={styles.copyButton}
>
Копировать ссылку
</button>
{showPopup && (
<div className={styles.popup}>
Ссылка скопирована
</div>
)}
</div>
)}
</div>
)
}

View file

@ -235,12 +235,13 @@ export const SurveyPage: React.FC = () => {
surveyId: id,
id: question.id,
title: question.text,
questionType: question.questionType // Убедитесь, что передается новый тип
questionType: question.questionType
} as QuestionActionData & { id: number }
});
}
});
questions.forEach(question => {
question.answerVariants.forEach(answer => {
if (!answer.id) {