From 69f7491cf8e30e692e98382de42f66fabe243986 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 23:19:29 +0000
Subject: [PATCH 1/6] Initial plan
From 6c31abc6b15f272ffbf3c71913bae29bf68377d3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 00:01:28 +0000
Subject: [PATCH 2/6] Implement complete fatigue assessment test feature
Co-authored-by: dedmamn <122568386+dedmamn@users.noreply.github.com>
---
src/App.tsx | 57 ++++
src/components/FirstTimeTestPopup.tsx | 120 +++++++++
src/components/TestModal.tsx | 338 ++++++++++++++++++++++++
src/components/TestResults.tsx | 358 ++++++++++++++++++++++++++
src/data/testData.ts | 164 ++++++++++++
src/hooks/useDataManager.ts | 7 +-
src/hooks/useTestManager.ts | 150 +++++++++++
src/pages/Home.tsx | 70 ++++-
src/pages/Settings.tsx | 22 +-
src/types/index.ts | 49 ++++
src/utils/dataMigration.ts | 7 +-
src/utils/storage.ts | 7 +-
src/utils/testUtils.ts | 99 +++++++
13 files changed, 1430 insertions(+), 18 deletions(-)
create mode 100644 src/components/FirstTimeTestPopup.tsx
create mode 100644 src/components/TestModal.tsx
create mode 100644 src/components/TestResults.tsx
create mode 100644 src/data/testData.ts
create mode 100644 src/hooks/useTestManager.ts
create mode 100644 src/utils/testUtils.ts
diff --git a/src/App.tsx b/src/App.tsx
index 5459cb9..185f4b9 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,7 +7,11 @@ import Activities from './pages/Activities';
import Settings from './pages/Settings';
import Navigation from './components/Navigation';
import BackupReminder from './components/BackupReminder';
+import TestModal from './components/TestModal';
+import TestResults from './components/TestResults';
+import FirstTimeTestPopup from './components/FirstTimeTestPopup';
import { useDataManager } from './hooks/useDataManager';
+import { useTestManager } from './hooks/useTestManager';
import { useNotifications } from './hooks/useNotifications';
import { testLocalStorage, debugLocalStorage } from './utils/storageTest';
import './styles/global.css';
@@ -19,12 +23,29 @@ const App = () => {
settings,
setActivities,
setSettings,
+ addActivity,
createBackup
} = useDataManager();
// Инициализация системы уведомлений
const { sendTestNotification, getNotificationStatus, clearNotifications } = useNotifications({ settings });
+ // Инициализация системы тестов
+ const {
+ isTestModalOpen,
+ isResultsModalOpen,
+ currentTestResult,
+ testHistory,
+ shouldShowFirstTimePopup,
+ openTestModal,
+ closeTestModal,
+ openResultsModal,
+ closeResultsModal,
+ completeTest,
+ markFirstTestCompleted,
+ addRecommendedActivities
+ } = useTestManager({ settings, setSettings, addActivity });
+
const theme = createTheme({
palette: {
mode: settings.theme,
@@ -79,6 +100,15 @@ const App = () => {
createBackup();
};
+ const handleStartFirstTest = () => {
+ markFirstTestCompleted();
+ openTestModal();
+ };
+
+ const handleSkipFirstTest = () => {
+ markFirstTestCompleted();
+ };
+
return (
@@ -106,6 +136,8 @@ const App = () => {
activities={activities}
setActivities={setActivities}
settings={settings}
+ onOpenTest={openTestModal}
+ testHistory={testHistory}
/>
}
/>
@@ -126,12 +158,37 @@ const App = () => {
setSettings={setSettings}
activities={activities}
setActivities={setActivities}
+ testHistory={testHistory}
+ onOpenTest={openTestModal}
+ onOpenTestResult={openResultsModal}
/>
}
/>
+
+ {/* Модальные окна тестов */}
+
+
+
+
+ {currentTestResult && (
+
+ )}
diff --git a/src/components/FirstTimeTestPopup.tsx b/src/components/FirstTimeTestPopup.tsx
new file mode 100644
index 0000000..3feb8bf
--- /dev/null
+++ b/src/components/FirstTimeTestPopup.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ Typography,
+ Box,
+ Avatar,
+ Card,
+ CardContent
+} from '@mui/material';
+import { Psychology, Close, QuestionAnswer } from '@mui/icons-material';
+
+interface FirstTimeTestPopupProps {
+ open: boolean;
+ onStartTest: () => void;
+ onSkip: () => void;
+}
+
+const FirstTimeTestPopup: React.FC = ({
+ open,
+ onStartTest,
+ onSkip
+}) => {
+ return (
+
+ );
+};
+
+export default FirstTimeTestPopup;
\ No newline at end of file
diff --git a/src/components/TestModal.tsx b/src/components/TestModal.tsx
new file mode 100644
index 0000000..486d971
--- /dev/null
+++ b/src/components/TestModal.tsx
@@ -0,0 +1,338 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ Typography,
+ Box,
+ RadioGroup,
+ FormControlLabel,
+ Radio,
+ FormControl,
+ FormLabel,
+ LinearProgress,
+ Chip,
+ Alert,
+ Divider,
+ IconButton
+} from '@mui/material';
+import { Close, Psychology, NavigateNext, NavigateBefore } from '@mui/icons-material';
+import { TestQuestion, TestAnswer } from '../types';
+import { testQuestions, testInstructions } from '../data/testData';
+import { shuffleQuestions } from '../utils/testUtils';
+
+interface TestModalProps {
+ open: boolean;
+ onClose: () => void;
+ onComplete: (answers: TestAnswer[]) => void;
+ showWarning?: boolean;
+}
+
+const TestModal: React.FC = ({ open, onClose, onComplete, showWarning = true }) => {
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
+ const [answers, setAnswers] = useState([]);
+ const [shuffledQuestions, setShuffledQuestions] = useState([]);
+ const [showInstructions, setShowInstructions] = useState(true);
+ const [hasShownWarning, setHasShownWarning] = useState(false);
+
+ // Перемешиваем вопросы при открытии теста
+ useEffect(() => {
+ if (open) {
+ setShuffledQuestions(shuffleQuestions(testQuestions));
+ setCurrentQuestionIndex(0);
+ setAnswers([]);
+ setShowInstructions(true);
+ setHasShownWarning(false);
+ }
+ }, [open]);
+
+ const currentQuestion = shuffledQuestions[currentQuestionIndex];
+ const progress = ((currentQuestionIndex + 1) / shuffledQuestions.length) * 100;
+
+ const handleAnswerChange = (questionId: number, score: number) => {
+ setAnswers(prev => {
+ const existing = prev.find(a => a.questionId === questionId);
+ if (existing) {
+ return prev.map(a =>
+ a.questionId === questionId ? { ...a, score } : a
+ );
+ } else {
+ return [...prev, { questionId, score }];
+ }
+ });
+ };
+
+ const getCurrentAnswer = (): number | undefined => {
+ return answers.find(a => a.questionId === currentQuestion?.id)?.score;
+ };
+
+ const canGoNext = (): boolean => {
+ if (!currentQuestion) return false;
+ return getCurrentAnswer() !== undefined;
+ };
+
+ const handleNext = () => {
+ if (currentQuestionIndex < shuffledQuestions.length - 1) {
+ setCurrentQuestionIndex(prev => prev + 1);
+ } else {
+ // Проверяем, что все вопросы отвечены
+ const allAnswered = shuffledQuestions.every(q =>
+ answers.some(a => a.questionId === q.id)
+ );
+
+ if (allAnswered) {
+ onComplete(answers);
+ }
+ }
+ };
+
+ const handlePrevious = () => {
+ if (currentQuestionIndex > 0) {
+ setCurrentQuestionIndex(prev => prev - 1);
+ }
+ };
+
+ const handleClose = () => {
+ if (!hasShownWarning && answers.length > 0 && showWarning) {
+ setHasShownWarning(true);
+ return;
+ }
+ onClose();
+ };
+
+ const handleStartTest = () => {
+ setShowInstructions(false);
+ };
+
+ const answeredCount = answers.length;
+ const isLastQuestion = currentQuestionIndex === shuffledQuestions.length - 1;
+ const allAnswered = shuffledQuestions.length > 0 &&
+ shuffledQuestions.every(q => answers.some(a => a.questionId === q.id));
+
+ if (showInstructions) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default TestModal;
\ No newline at end of file
diff --git a/src/components/TestResults.tsx b/src/components/TestResults.tsx
new file mode 100644
index 0000000..1d68308
--- /dev/null
+++ b/src/components/TestResults.tsx
@@ -0,0 +1,358 @@
+import React, { useState } from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ Typography,
+ Box,
+ Card,
+ CardContent,
+ LinearProgress,
+ Chip,
+ Alert,
+ Divider,
+ IconButton,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Fab
+} from '@mui/material';
+import {
+ Close,
+ CheckCircle,
+ ExpandMore,
+ Psychology,
+ LocalHospital,
+ Spa,
+ Download,
+ Add
+} from '@mui/icons-material';
+import { FatigueType, TestResult, Activity } from '../types';
+import { getScoreInterpretation, shouldShowMedicalWarning, formatTestDate } from '../utils/testUtils';
+
+interface TestResultsProps {
+ open: boolean;
+ onClose: () => void;
+ testResult: TestResult;
+ onAddActivitiesToCalendar?: (activities: Partial[]) => void;
+}
+
+const TestResults: React.FC = ({
+ open,
+ onClose,
+ testResult,
+ onAddActivitiesToCalendar
+}) => {
+ const [expandedType, setExpandedType] = useState(false);
+ const { dominantTypes, fatigueScores, completedAt } = testResult;
+
+ const handleAccordionChange = (panel: string) => (
+ event: React.SyntheticEvent,
+ isExpanded: boolean
+ ) => {
+ setExpandedType(isExpanded ? panel : false);
+ };
+
+ const getScoreColor = (score: number) => {
+ if (score <= 10) return 'success';
+ if (score <= 20) return 'warning';
+ return 'error';
+ };
+
+ const handleAddRecommendationsToCalendar = () => {
+ if (!onAddActivitiesToCalendar) return;
+
+ const recommendedActivities: Partial[] = dominantTypes.flatMap(type =>
+ type.restActivities.slice(0, 3).map(activityName => ({
+ name: activityName,
+ description: `Рекомендовано для восстановления от ${type.name.toLowerCase()}`,
+ type: 'physical' as any, // Можно сделать более умный выбор типа
+ duration: 30,
+ isActive: true,
+ createdAt: new Date(),
+ completedDates: []
+ }))
+ );
+
+ onAddActivitiesToCalendar(recommendedActivities);
+ };
+
+ const exportResults = () => {
+ const resultsText = `
+РЕЗУЛЬТАТЫ ТЕСТА НА УСТАЛОСТЬ
+Дата прохождения: ${formatTestDate(completedAt)}
+
+ДОМИНИРУЮЩИЕ ТИПЫ УСТАЛОСТИ:
+${dominantTypes.map((type, index) => `
+${index + 1}. ${type.name} (${type.score} баллов - ${getScoreInterpretation(type.score)})
+
+${type.description}
+
+${type.solutions}
+
+Рекомендованные виды отдыха:
+${type.restActivities.map(activity => `• ${activity}`).join('\n')}
+`).join('\n---\n')}
+
+ПОЛНЫЕ РЕЗУЛЬТАТЫ ПО ВСЕМ ТИПАМ:
+${Object.entries(fatigueScores).map(([type, score]) =>
+ `${type}: ${score} баллов (${getScoreInterpretation(score)})`
+).join('\n')}
+
+${shouldShowMedicalWarning(dominantTypes) ?
+`⚠️ ВАЖНО: Для типов усталости СХУ и гормональная усталость с высокими баллами (>15) требуется консультация врача.` :
+''
+}
+
+Ключевое правило: Если симптомы длятся > 2 месяцев и мешают жить — обратитесь к врачу (терапевт, эндокринолог, невролог) или психотерапевту. Усталость — не ваша вина, но ваша ответственность. Начните с малого: выберите 1–2 пункта из рекомендаций и внедряйте их 21 день. Тело и психика скажут спасибо! 💫
+ `.trim();
+
+ const blob = new Blob([resultsText], { type: 'text/plain;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `результаты-теста-усталости-${new Date().toISOString().split('T')[0]}.txt`;
+ link.click();
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+ );
+};
+
+export default TestResults;
\ No newline at end of file
diff --git a/src/data/testData.ts b/src/data/testData.ts
new file mode 100644
index 0000000..2eb1fc5
--- /dev/null
+++ b/src/data/testData.ts
@@ -0,0 +1,164 @@
+import { TestQuestion, FatigueType } from '../types';
+
+export const testQuestions: TestQuestion[] = [
+ // Блок А: Физические симптомы (1-9)
+ { id: 1, block: 'A', blockName: 'Физические симптомы', text: 'Головная боль' },
+ { id: 2, block: 'A', blockName: 'Физические симптомы', text: 'Мышечная слабость (ощущение "ватных" ног/рук)' },
+ { id: 3, block: 'A', blockName: 'Физические симптомы', text: 'Мышечное напряжение или боли (без явной причины)' },
+ { id: 4, block: 'A', blockName: 'Физические симптомы', text: 'Ощущение общего физического утомления (тяжело двигаться)' },
+ { id: 5, block: 'A', blockName: 'Физические симптомы', text: 'Проблемы с пищеварением (несварение, запор, диарея)' },
+ { id: 6, block: 'A', blockName: 'Физические симптомы', text: 'Частые простуды, ОРВИ, обострения хронических болезней' },
+ { id: 7, block: 'A', blockName: 'Физические симптомы', text: 'Сонливость в течение дня, несмотря на сон' },
+ { id: 8, block: 'A', blockName: 'Физические симптомы', text: 'Колебания веса (не связанные с диетой)' },
+ { id: 9, block: 'A', blockName: 'Физические симптомы', text: 'Снижение либидо (полового влечения)' },
+
+ // Блок Б: Эмоциональные и ментальные симптомы (10-21)
+ { id: 10, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Апатия (ничего не хочется, безразличие)' },
+ { id: 11, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Раздражительность, вспыльчивость' },
+ { id: 12, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Тоска, беспричинная грусть' },
+ { id: 13, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Ощущение внутренней пустоты' },
+ { id: 14, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Чувство, что "внутри кто-то плачет"' },
+ { id: 15, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Отсутствие желаний, интересов' },
+ { id: 16, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Серый, безрадостный эмоциональный фон' },
+ { id: 17, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Плаксивость' },
+ { id: 18, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Ощущение ненужности' },
+ { id: 19, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Падение самооценки' },
+ { id: 20, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Тревога, беспокойство, предчувствие беды' },
+ { id: 21, block: 'B', blockName: 'Эмоциональные и ментальные симптомы', text: 'Ощущение западни, ловушки' },
+
+ // Блок В: Когнитивные (мыслительные) симптомы (22-27)
+ { id: 22, block: 'C', blockName: 'Когнитивные (мыслительные) симптомы', text: 'Трудности концентрации внимания (легко отвлекаюсь)' },
+ { id: 23, block: 'C', blockName: 'Когнитивные (мыслительные) симптомы', text: '"Туман в голове" (мысли вязкие, неясные)' },
+ { id: 24, block: 'C', blockName: 'Когнитивные (мыслительные) симптомы', text: 'Ухудшение памяти (забываю недавние события)' },
+ { id: 25, block: 'C', blockName: 'Когнитивные (мыслительные) симптомы', text: 'Замедление мыслительных процессов ("туго соображаю")' },
+ { id: 26, block: 'C', blockName: 'Когнитивные (мыслительные) симптомы', text: 'Снижение продуктивности, работоспособности' },
+ { id: 27, block: 'C', blockName: 'Когнитивные (мыслительные) симптомы', text: 'Невозможность принять решение' },
+
+ // Блок Г: Симптомы отношений и энергии (28-34)
+ { id: 28, block: 'D', blockName: 'Симптомы отношений и энергии', text: 'Опустошенность, истощение (физическое/эмоциональное)' },
+ { id: 29, block: 'D', blockName: 'Симптомы отношений и энергии', text: 'Бессилие (ощущение, что нет сил даже на мелкие дела)' },
+ { id: 30, block: 'D', blockName: 'Симптомы отношений и энергии', text: 'Низкий общий уровень энергии' },
+ { id: 31, block: 'D', blockName: 'Симптомы отношений и энергии', text: 'Желание изолироваться от людей' },
+ { id: 32, block: 'D', blockName: 'Симптомы отношений и энергии', text: 'Раздражительность при общении' },
+ { id: 33, block: 'D', blockName: 'Симптомы отношений и энергии', text: 'Ощущение, что заботиться о других — тяжелая обязанность' },
+ { id: 34, block: 'D', blockName: 'Симптомы отношений и энергии', text: 'Утрата смыслов (зачем я это делаю?)' },
+
+ // Блок Д: Физиологические симптомы (35-40)
+ { id: 35, block: 'E', blockName: 'Физиологические симптомы', text: 'Проблемы с засыпанием или сном' },
+ { id: 36, block: 'E', blockName: 'Физиологические симптомы', text: 'Изменения аппетита (сильное снижение или повышение)' },
+ { id: 37, block: 'E', blockName: 'Физиологические симптомы', text: 'Снижение физической активности (двигаюсь меньше обычного)' },
+ { id: 38, block: 'E', blockName: 'Физиологические симптомы', text: 'Ортостатические проблемы (головокружение при вставании)' },
+ { id: 39, block: 'E', blockName: 'Физиологические симптомы', text: 'Непереносимость яркого света или громких звуков' },
+ { id: 40, block: 'E', blockName: 'Физиологические симптомы', text: 'Длительная субфебрильная температура (37.0 - 37.5°C)' }
+];
+
+// Группировка вопросов по типам усталости
+export const fatigueTypeQuestions = {
+ physical: [1, 2, 3, 4, 5, 6, 7, 37],
+ emotional: [10, 11, 12, 13, 14, 15, 16, 17, 20],
+ cognitive: [22, 23, 24, 25, 26, 27],
+ social: [20, 21, 31, 32],
+ achievement: [15, 16, 17, 26, 30, 34],
+ caregiving: [18, 19, 28, 30, 33],
+ anxiety: [7, 20, 21, 22, 23, 28, 29, 35, 36],
+ hormonal: [7, 8, 9, 35, 36, 38],
+ chronicFatigue: [1, 2, 3, 4, 6, 7, 22, 23, 24, 25, 26, 28, 29, 30, 35, 36, 38, 39, 40]
+};
+
+export const fatigueTypeDescriptions: Record> = {
+ physical: {
+ type: 'physical',
+ name: 'Физическая усталость',
+ description: `Откуда берется:
+Эта усталость — крик вашего тела о помощи. Ее вызывает не просто движение, а чрезмерная или непривычная нагрузка: долгая работа «на ногах», интенсивные тренировки без восстановления, физический труд. Но причина не только в активности! Тихие провокаторы — анемия (нехватка железа), дефицит витаминов группы B или D, скрытые воспаления, хроническое недосыпание. Тело исчерпало ресурсы и не успевает восстанавливаться.`,
+ solutions: `Как избавиться:
+Слушайте тело: дайте ему отдых! Чередуйте нагрузки с расслаблением — теплые ванны, массаж, качественный сон (7–9 часов). Включите умеренную активность: ходьба, плавание, йога улучшат выносливость. Проверьте здоровье: сдайте анализы на гемоглобин, ферритин, витамины. Скорректируйте питание: белок, сложные углеводы, овощи, вода. Важно не «ломать» себя, а восстанавливать ритм.`,
+ restActivities: ['Глубокий сон', 'Теплые ванны (с солью/маслами и простые)', 'Массаж/самомассаж', 'Йога/стретчинг', 'Легкая ходьба', 'Скандинавская ходьба', 'Плавание', 'Сауна/баня', 'Здоровая еда (сбалансированное питание)', 'Умеренные силовые тренировки', 'Бег/велосипед (низкая интенсивность)', 'Бездействие (осознанный отдых лежа)']
+ },
+ caregiving: {
+ type: 'caregiving',
+ name: 'Усталость от заботы о других',
+ description: `Откуда берется:
+Это истощение родом из нарушенных границ и потери себя. Вы отдаете энергию, время, внимание — детям, партнеру, родителям, друзьям — но забываете о себе. Подпитывает это: убеждение «я должен/должна», страх отвержения, чувство вины за «эгоизм», созависимость. Подавленный гнев («Я устал, но молчу») и дисбаланс «брать-давать» превращают заботу в тяжкую ношу.`,
+ solutions: `Как избавиться:
+Верните себя в центр: «Разрешено заботиться о себе!». Учитесь говорить «нет», делегируйте, просите о помощи. Работа с психологом поможет разорвать созависимые схемы. Практикуйте micro-self-care: 15 минут в день — только ваши желания (чай в тишине, музыка, хобби). Напоминайте: вы полезны другим, лишь когда наполнены сами.`,
+ restActivities: ['Self-care ритуалы', '"Запретная" радость (книга в постели)', 'Занятия "для себя" (курсы, спорт)', 'Отдых вне дома', 'Гастротуры', 'Игра на музыкальных инструментах (для удовольствия)', 'Практики благодарности (ежедневный список)']
+ },
+ cognitive: {
+ type: 'cognitive',
+ name: 'Когнитивная усталость',
+ description: `Откуда берется:
+Ваш мозг перегружен как перегретый процессор. Информационный шквал (соцсети, новости, рабочие чаты), мультизадачность и непрерывная концентрация без перерывов истощают нейронные сети. Добавьте сюда недосып — и вот уже «туман в голове», провалы в памяти. Это не лень, а реальная перегрузка когнитивных функций.`,
+ solutions: `Как избавиться:
+Цифровой детокс: отключайте уведомления, выделите «тихие часы» без гаджетов. Дробите задачи: метод Pomodoro (25 мин. работы / 5 мин. отдыха). Фиксируйте мысли на бумаге — освобождайте «оперативку». Приоритезируйте: делайте сначала 1 ключевое дело, а не 10 мелких. Сон и физическая активность (даже прогулка!) улучшают нейропластичность.`,
+ restActivities: ['Цифровой детокс', 'Медитация/дыхание 4-7-8', 'Сбор пазлов, рукоделие', 'Легкое чтение', 'Короткие перерывы каждые 90 мин (техника Pomodoro)', 'Решение головоломок', 'Тишина (сеансы без звука)', 'Спокойная музыка (классика, ambient)']
+ },
+ achievement: {
+ type: 'achievement',
+ name: 'Усталость от достижений',
+ description: `Откуда берется:
+Это плата за перфекционизм и вечную гонку. Внутренний тиран шепчет: «Еще выше! Быстрее! Идеально!». Вы ставите нереальные цели, игнорируете успехи («Это не счет»), сравниваете себя с другими. Радость подменяется страхом ошибки. Истощение приходит, когда смысл теряется, а энергия на нуле — вы больше не верите в свою гонку.`,
+ solutions: `Как избавиться:
+Смените фокус: цените процесс, а не только результат. Снизьте планку: разрешите себе быть «достаточно хорошим». Фиксируйте и празднуйте микро-победы (записывайте 3 достижения за день). Практикуйте благодарность себе. Терапия поможет усмирить внутреннего критика. Помните: ваша ценность — не в титулах, а в вас самих.`,
+ restActivities: ['Осознанное ничегонеделание', 'Игры без цели', 'Спонтанные удовольствия', 'Мотивационные ролики', 'Посещение выставок/концертов (пассивное участие)', 'Рукоделие (без перфекционизма)', 'Фотография (прогулки с камерой)']
+ },
+ anxiety: {
+ type: 'anxiety',
+ name: 'Тревожная усталость',
+ description: `Откуда берется:
+Это невидимая война внутри. Фоновая тревога «что-то пойдет не так», навязчивые мысли («А вдруг?..»), попытки контролировать неконтролируемое (будущее, чувства других) съедают ресурсы. Мозг в режиме вечной «тревожной сигнализации» сжигает энергию даже в покое. Результат — опустошенность, бессилие, «туман».`,
+ solutions: `Как избавиться:
+Останавливайте «мыслемешалку»: техники заземления («5-4-3-2-1» — что вижу/слышу/ощущаю), дыхание 4-7-8 (вдох на 4, задержка на 7, выдох на 8). Терапия (КПТ, ACT) научит управлять тревогой. Снижайте неопределенность: планируйте ближайшие шаги, но принимайте, что 100% контроля — иллюзия. Физическая активность (бег, танцы) «сжигает» кортизол.`,
+ restActivities: ['Заземление 5-4-3-2-1', 'Монотонные действия (раскраски, мытье посуды)', 'ASMR/релакс-музыка', 'Теплое питье', 'Снижение яркости экранов', 'Отключение уведомлений', 'Практики осознанного восприятия (еда, запахи)', 'Сеансы в темноте']
+ },
+ emotional: {
+ type: 'emotional',
+ name: 'Эмоциональная усталость',
+ description: `Откуда берется:
+Вы — выжатый эмоциональный «лимон». Причины: длительный стресс (конфликты, токсичная среда), подавление чувств («Надо терпеть»), эмпатия без границ (пропускаете чужую боль через себя). Опустошение, апатия, чувство «внутри плачет» — знаки, что эмоциональный резерв исчерпан. Вы больше не можете сопереживать — это самозащита.`,
+ solutions: `Как избавиться:
+Эмоциональный детокс: ограничьте общение с «вампирами», сократите просмотр триггерного контента. Проговаривайте чувства: дневник, терапия, доверительные разговоры. Практикуйте осознанность: «Что я чувствую сейчас? Где в теле это ощущается?». Ищите источники радости: музыка, природа, творчество. Важно не «закрыться», а восстановить баланс.`,
+ restActivities: ['Ведение дневника чувств', 'Арт-терапия', 'Просмотр легких фильмов', 'Общение с питомцем', 'Природа (лес, вода)', 'Практики прощения (письма, медитации)', 'Дневник самосострадания', 'Акты доброты (помощь другим без напряжения)', 'Крик-терапия']
+ },
+ social: {
+ type: 'social',
+ name: 'Социальная усталость',
+ description: `Откуда берется:
+После общения вам нужно «отлежаться» сутки? Виновники — перегруз коммуникациями: митинги, светские беседы, переписки в 10 мессенджерах. Токсичные люди, нарушение ваших границ («Вы должны выслушать!»), FOMO (страх пропустить важное в соцсетях) и роль «удобного человека» высасывают силы. Вы не интроверт — вы перенасытились контактами.`,
+ solutions: `Как избавиться:
+Жесткие границы: «Чат-ланч» с 13:00 до 14:00, «тихий час» без телефона. Учитесь говорить: «Сейчас не могу, отвечу позже». Фильтруйте окружение: минимизируйте контакты с теми, после кого чувствуете опустошение. Цифровой пост: 1 день в неделю без соцсетей. Планируйте дни «только для себя» без угрызений совести. Общение должно питать, не вытягивать.`,
+ restActivities: ['День тишины', 'Соло-активности (кафе, музей)', '"Ленивое" хобби (вышивка, рыбалка)', 'Лесные прогулки в одиночестве', 'Волонтерство (в комфортных проектах)', 'Игры с доверенной группой (настолки, квизы)', 'Позитивные соцсети (подписки на вдохновляющие аккаунты)', 'Просьба о помощи (делегирование задач)']
+ },
+ hormonal: {
+ type: 'hormonal',
+ name: 'Гормональная усталость',
+ description: `Откуда берется:
+Это усталость «изнутри системы». Дисбаланс гормонов (щитовидки — ТТГ, Т3, Т4; половых — эстроген, прогестерон, тестостерон; кортизол) нарушает энергообмен. Симптомы: сонливость днем/бессонница ночью, вес «стоит» или растет, ПМС как катастрофа, плаксивость. Стресс, недосып, диеты, воспаления — частые триггеры сбоев.`,
+ solutions: `Как избавиться:
+Не гадайте — обследуйтесь! Анализы: ТТГ + свободный Т4, кортизол, эстрадиол, прогестерон (для женщин — на 19–21 день цикла), витамин D, ферритин. Консультация эндокринолога обязательна. Поддержите тело: режим сна до 23:00, питание с жирами (гормоны из холестерина!), снижение стресса (йога, дыхание). Самолечение опасно!`,
+ restActivities: ['Сон по циркадным ритмам', 'Тепло на живот (грелка)', 'Йога-нидра', 'Прогулки на рассвете/закате', 'Наблюдение за звездами', 'Чтение духовной литературы', 'Ретриты/паломничество (краткосрочные)', 'Уникальные места (места силы)']
+ },
+ chronicFatigue: {
+ type: 'chronicFatigue',
+ name: 'СХУ (комплексная усталость)',
+ description: `Откуда берется:
+Комплексная усталость (СХУ) — это мультисистемное расстройство, которое объединяет физические, когнитивные и эмоциональные симптомы. Это не просто усталость, а серьезное состояние, требующее медицинского наблюдения. СХУ может развиваться после инфекций, стрессов, травм или на фоне гормональных нарушений.`,
+ solutions: `Как избавиться:
+Обязательна консультация врача для исключения других заболеваний и составления плана лечения. Важен режим дня с дозированными нагрузками, избегание переутомления. Постепенное увеличение активности под контролем специалистов. Поддерживающая терапия, коррекция дефицитов витаминов и минералов.`,
+ restActivities: ['Глубокий сон', 'Медитация/дыхание 4-7-8', 'Легкая ходьба', 'Теплые ванны', 'Тишина (сеансы без звука)', 'Бездействие (осознанный отдых лежа)', 'Йога-нидра', 'Сон по циркадным ритмам']
+ }
+};
+
+// Текст инструкции для теста
+export const testInstructions = {
+ title: 'Пройдите тест для определения преобладающих видов усталости и получите индивидуальные рекомендации по отдыху.',
+ instructions: `Оцените, насколько часто вы испытывали каждый из перечисленных симптомов за последние 2 недели:
+
+0 = Никогда / почти никогда
+1 = Редко (1-2 раза)
+2 = Иногда (3-4 раза)
+3 = Часто
+4 = Очень часто
+5 = Постоянно`,
+ warning: 'Внимание: для получения корректных результатов необходимо ответить на все вопросы. Тест можно пройти только полностью.'
+};
\ No newline at end of file
diff --git a/src/hooks/useDataManager.ts b/src/hooks/useDataManager.ts
index 051c9c3..a996be1 100644
--- a/src/hooks/useDataManager.ts
+++ b/src/hooks/useDataManager.ts
@@ -22,7 +22,12 @@ const defaultSettings: Settings = {
notificationsEnabled: true,
theme: 'light',
defaultActivityDuration: 30,
- reminderTime: '09:00'
+ reminderTime: '09:00',
+ testSettings: {
+ hasCompletedFirstTest: false,
+ showTestReminderPopup: true,
+ testHistory: []
+ }
};
export const useDataManager = (): UseDataManagerReturn => {
diff --git a/src/hooks/useTestManager.ts b/src/hooks/useTestManager.ts
new file mode 100644
index 0000000..e3f296e
--- /dev/null
+++ b/src/hooks/useTestManager.ts
@@ -0,0 +1,150 @@
+import { useState, useCallback } from 'react';
+import { TestAnswer, TestResult, Settings, Activity, ActivityType } from '../types';
+import { createTestResult } from '../utils/testUtils';
+
+interface UseTestManagerProps {
+ settings: Settings;
+ setSettings: React.Dispatch>;
+ addActivity?: (activity: Omit) => void;
+}
+
+interface UseTestManagerReturn {
+ isTestModalOpen: boolean;
+ isResultsModalOpen: boolean;
+ currentTestResult: TestResult | null;
+ testHistory: TestResult[];
+ shouldShowFirstTimePopup: boolean;
+ openTestModal: () => void;
+ closeTestModal: () => void;
+ openResultsModal: (result: TestResult) => void;
+ closeResultsModal: () => void;
+ completeTest: (answers: TestAnswer[]) => void;
+ markFirstTestCompleted: () => void;
+ addRecommendedActivities: (activities: Partial[]) => void;
+}
+
+export const useTestManager = ({
+ settings,
+ setSettings,
+ addActivity
+}: UseTestManagerProps): UseTestManagerReturn => {
+ const [isTestModalOpen, setIsTestModalOpen] = useState(false);
+ const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
+ const [currentTestResult, setCurrentTestResult] = useState(null);
+
+ const testHistory = settings.testSettings.testHistory;
+ const shouldShowFirstTimePopup = !settings.testSettings.hasCompletedFirstTest &&
+ settings.testSettings.showTestReminderPopup;
+
+ const openTestModal = useCallback(() => {
+ setIsTestModalOpen(true);
+ }, []);
+
+ const closeTestModal = useCallback(() => {
+ setIsTestModalOpen(false);
+ }, []);
+
+ const openResultsModal = useCallback((result: TestResult) => {
+ setCurrentTestResult(result);
+ setIsResultsModalOpen(true);
+ }, []);
+
+ const closeResultsModal = useCallback(() => {
+ setIsResultsModalOpen(false);
+ setCurrentTestResult(null);
+ }, []);
+
+ const completeTest = useCallback((answers: TestAnswer[]) => {
+ const testResultData = createTestResult(answers);
+ const testResult: TestResult = {
+ id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
+ ...testResultData
+ };
+
+ // Сохраняем результат в историю
+ setSettings(prev => ({
+ ...prev,
+ testSettings: {
+ ...prev.testSettings,
+ hasCompletedFirstTest: true,
+ showTestReminderPopup: false,
+ testHistory: [testResult, ...prev.testSettings.testHistory]
+ }
+ }));
+
+ // Закрываем тест и показываем результаты
+ setIsTestModalOpen(false);
+ openResultsModal(testResult);
+ }, [setSettings, openResultsModal]);
+
+ const markFirstTestCompleted = useCallback(() => {
+ setSettings(prev => ({
+ ...prev,
+ testSettings: {
+ ...prev.testSettings,
+ hasCompletedFirstTest: true,
+ showTestReminderPopup: false
+ }
+ }));
+ }, [setSettings]);
+
+ const addRecommendedActivities = useCallback((activities: Partial[]) => {
+ if (!addActivity) return;
+
+ activities.forEach(activityData => {
+ // Определяем тип активности на основе названия
+ let activityType: ActivityType = ActivityType.PHYSICAL;
+ const name = activityData.name?.toLowerCase() || '';
+
+ if (name.includes('дневник') || name.includes('терапия') || name.includes('эмоци')) {
+ activityType = ActivityType.EMOTIONAL;
+ } else if (name.includes('медитация') || name.includes('чтение') || name.includes('детокс')) {
+ activityType = ActivityType.MENTAL;
+ } else if (name.includes('общение') || name.includes('социальн') || name.includes('волонтер')) {
+ activityType = ActivityType.SOCIAL;
+ } else if (name.includes('творчество') || name.includes('арт') || name.includes('музык')) {
+ activityType = ActivityType.CREATIVE;
+ } else if (name.includes('природа') || name.includes('прогулк') || name.includes('лес')) {
+ activityType = ActivityType.OUTDOOR;
+ } else if (name.includes('сон') || name.includes('отдых') || name.includes('релакс')) {
+ activityType = ActivityType.PASSIVE;
+ } else if (name.includes('звезды') || name.includes('духовн') || name.includes('ретрит')) {
+ activityType = ActivityType.SPIRITUAL;
+ } else if (name.includes('массаж') || name.includes('ванна') || name.includes('ASMR')) {
+ activityType = ActivityType.SENSORY;
+ }
+
+ const newActivity: Omit = {
+ type: activityType,
+ name: activityData.name || 'Рекомендованная активность',
+ description: activityData.description || 'Импортировано из результатов теста на усталость',
+ duration: activityData.duration || 30,
+ isActive: true,
+ createdAt: new Date(),
+ completedDates: [],
+ recurrence: {
+ frequency: 'weekly',
+ interval: 1,
+ daysOfWeek: [1, 3, 5] // Понедельник, среда, пятница
+ }
+ };
+
+ addActivity(newActivity);
+ });
+ }, [addActivity]);
+
+ return {
+ isTestModalOpen,
+ isResultsModalOpen,
+ currentTestResult,
+ testHistory,
+ shouldShowFirstTimePopup,
+ openTestModal,
+ closeTestModal,
+ openResultsModal,
+ closeResultsModal,
+ completeTest,
+ markFirstTestCompleted,
+ addRecommendedActivities
+ };
+};
\ No newline at end of file
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 9b34c5e..49a9bf7 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -7,10 +7,12 @@ import {
Card,
CardContent,
LinearProgress,
- Avatar
+ Avatar,
+ Button,
+ IconButton
} from '@mui/material';
-import { Add, TrendingUp, Today, Assignment } from '@mui/icons-material';
-import { Activity, Settings as SettingsType } from '../types';
+import { Add, TrendingUp, Today, Assignment, Psychology } from '@mui/icons-material';
+import { Activity, Settings as SettingsType, TestResult } from '../types';
import ActivityCard from '../components/ActivityCard';
import ActivityForm from '../components/ActivityForm';
@@ -18,9 +20,17 @@ interface HomeProps {
activities: Activity[];
setActivities: React.Dispatch>;
settings: SettingsType;
+ onOpenTest?: () => void;
+ testHistory?: TestResult[];
}
-const Home: React.FC = ({ activities, setActivities, settings }) => {
+const Home: React.FC = ({
+ activities,
+ setActivities,
+ settings,
+ onOpenTest,
+ testHistory = []
+}) => {
const [isFormOpen, setIsFormOpen] = useState(false);
const handleCompleteActivity = (activityId: string) => {
@@ -133,14 +143,50 @@ const Home: React.FC = ({ activities, setActivities, settings }) => {
flexDirection: 'column',
alignItems: 'center'
}}>
- {/* Приветствие */}
-
-
- {getGreeting()}
-
-
- Время позаботиться о своем отдыхе
-
+ {/* Приветствие с кнопкой теста */}
+
+
+
+
+ {getGreeting()}
+
+
+ Время позаботиться о своем отдыхе
+
+
+
+ {onOpenTest && (
+
+
+
+
+
+ Пройти
тест
+
+ {testHistory.length > 0 && (
+
+ {testHistory.length}
+
+ )}
+
+ )}
+
{/* Статистика */}
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index ca3bbc4..31fa60e 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -40,7 +40,7 @@ import {
Storage,
Backup
} from '@mui/icons-material';
-import { Settings as SettingsType, Activity } from '../types';
+import { Settings as SettingsType, Activity, TestResult } from '../types';
import { StorageManager } from '../utils/storage';
import { DataFormatter, NotificationManager } from '../utils/helpers';
import PWAInstall from '../components/PWAInstall';
@@ -50,9 +50,20 @@ interface SettingsProps {
setSettings: React.Dispatch>;
activities?: Activity[];
setActivities?: React.Dispatch>;
+ testHistory?: TestResult[];
+ onOpenTest?: () => void;
+ onOpenTestResult?: (result: TestResult) => void;
}
-const Settings: React.FC = ({ settings, setSettings, activities = [], setActivities }) => {
+const Settings: React.FC = ({
+ settings,
+ setSettings,
+ activities = [],
+ setActivities,
+ testHistory = [],
+ onOpenTest,
+ onOpenTestResult
+}) => {
const [exportDialogOpen, setExportDialogOpen] = useState(false);
const [importDialogOpen, setImportDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -116,7 +127,12 @@ const Settings: React.FC = ({ settings, setSettings, activities =
notificationsEnabled: true,
theme: 'light',
defaultActivityDuration: 30,
- reminderTime: '09:00'
+ reminderTime: '09:00',
+ testSettings: {
+ hasCompletedFirstTest: false,
+ showTestReminderPopup: true,
+ testHistory: []
+ }
});
setDeleteDialogOpen(false);
setStorageInfo(StorageManager.getStorageInfo());
diff --git a/src/types/index.ts b/src/types/index.ts
index f943441..225adcb 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -34,6 +34,7 @@ export interface Settings {
theme: 'light' | 'dark';
defaultActivityDuration: number;
reminderTime?: string; // время напоминания в формате HH:MM
+ testSettings: TestSettings;
}
export interface CompletedActivity {
@@ -41,4 +42,52 @@ export interface CompletedActivity {
completedAt: Date;
duration?: number;
notes?: string;
+}
+
+// Типы для теста усталости
+export interface TestQuestion {
+ id: number;
+ block: 'A' | 'B' | 'C' | 'D' | 'E';
+ blockName: string;
+ text: string;
+}
+
+export interface TestAnswer {
+ questionId: number;
+ score: number; // 0-5
+}
+
+export interface TestResult {
+ id: string;
+ completedAt: Date;
+ answers: TestAnswer[];
+ fatigueScores: FatigueScores;
+ dominantTypes: FatigueType[];
+}
+
+export interface FatigueScores {
+ physical: number;
+ emotional: number;
+ cognitive: number;
+ social: number;
+ achievement: number;
+ caregiving: number;
+ anxiety: number;
+ hormonal: number;
+ chronicFatigue: number;
+}
+
+export interface FatigueType {
+ type: keyof FatigueScores;
+ name: string;
+ score: number;
+ description: string;
+ solutions: string;
+ restActivities: string[];
+}
+
+export interface TestSettings {
+ hasCompletedFirstTest: boolean;
+ showTestReminderPopup: boolean;
+ testHistory: TestResult[];
}
\ No newline at end of file
diff --git a/src/utils/dataMigration.ts b/src/utils/dataMigration.ts
index 7d0b31f..9e2d902 100644
--- a/src/utils/dataMigration.ts
+++ b/src/utils/dataMigration.ts
@@ -155,7 +155,12 @@ export class DataMigration {
notificationsEnabled: true,
theme: 'light',
defaultActivityDuration: 30,
- reminderTime: '09:00'
+ reminderTime: '09:00',
+ testSettings: {
+ hasCompletedFirstTest: false,
+ showTestReminderPopup: true,
+ testHistory: []
+ }
};
}
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
index a400da4..b038128 100644
--- a/src/utils/storage.ts
+++ b/src/utils/storage.ts
@@ -263,7 +263,12 @@ export class StorageManager {
notificationsEnabled: true,
theme: 'light',
defaultActivityDuration: 30,
- reminderTime: '09:00'
+ reminderTime: '09:00',
+ testSettings: {
+ hasCompletedFirstTest: false,
+ showTestReminderPopup: true,
+ testHistory: []
+ }
};
}
diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts
new file mode 100644
index 0000000..8578abb
--- /dev/null
+++ b/src/utils/testUtils.ts
@@ -0,0 +1,99 @@
+import { TestAnswer, FatigueScores, FatigueType, TestResult } from '../types';
+import { fatigueTypeQuestions, fatigueTypeDescriptions } from '../data/testData';
+
+export const calculateFatigueScores = (answers: TestAnswer[]): FatigueScores => {
+ const scores: FatigueScores = {
+ physical: 0,
+ emotional: 0,
+ cognitive: 0,
+ social: 0,
+ achievement: 0,
+ caregiving: 0,
+ anxiety: 0,
+ hormonal: 0,
+ chronicFatigue: 0
+ };
+
+ // Создаем карту ответов для быстрого поиска
+ const answerMap = new Map();
+ answers.forEach(answer => {
+ answerMap.set(answer.questionId, answer.score);
+ });
+
+ // Подсчитываем баллы для каждого типа усталости
+ Object.entries(fatigueTypeQuestions).forEach(([type, questionIds]) => {
+ const totalScore = questionIds.reduce((sum, questionId) => {
+ return sum + (answerMap.get(questionId) || 0);
+ }, 0);
+ scores[type as keyof FatigueScores] = totalScore;
+ });
+
+ return scores;
+};
+
+export const getDominantFatigueTypes = (scores: FatigueScores, maxCount: number = 3): FatigueType[] => {
+ // Преобразуем баллы в массив с типами
+ const typesWithScores = Object.entries(scores).map(([typeKey, score]) => {
+ const typeDescription = fatigueTypeDescriptions[typeKey as keyof FatigueScores];
+ return {
+ ...typeDescription,
+ score
+ };
+ });
+
+ // Сортируем по убыванию баллов и берем только значимые (>= 11 баллов)
+ const significantTypes = typesWithScores
+ .filter(item => item.score >= 11) // Умеренная усталость начинается с 11 баллов
+ .sort((a, b) => b.score - a.score)
+ .slice(0, maxCount);
+
+ return significantTypes;
+};
+
+export const getScoreInterpretation = (score: number): string => {
+ if (score <= 10) return 'Норма';
+ if (score <= 20) return 'Умеренная усталость';
+ return 'Выраженная усталость';
+};
+
+export const shuffleQuestions = (array: T[]): T[] => {
+ const shuffled = [...array];
+ for (let i = shuffled.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+ }
+ return shuffled;
+};
+
+export const createTestResult = (answers: TestAnswer[]): Omit => {
+ const fatigueScores = calculateFatigueScores(answers);
+ const dominantTypes = getDominantFatigueTypes(fatigueScores);
+
+ return {
+ completedAt: new Date(),
+ answers,
+ fatigueScores,
+ dominantTypes
+ };
+};
+
+export const formatTestDate = (date: Date): string => {
+ return new Intl.DateTimeFormat('ru-RU', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ }).format(date);
+};
+
+export const getHighRiskTypes = (dominantTypes: FatigueType[]): string[] => {
+ const highRiskTypes = ['chronicFatigue', 'hormonal'];
+ return dominantTypes
+ .filter(type => highRiskTypes.includes(type.type) && type.score > 15)
+ .map(type => type.name);
+};
+
+export const shouldShowMedicalWarning = (dominantTypes: FatigueType[]): boolean => {
+ return getHighRiskTypes(dominantTypes).length > 0;
+};
\ No newline at end of file
From 0c92c71c260c159cbb369caaa68581939fe9bc85 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 00:04:32 +0000
Subject: [PATCH 3/6] Add test history section to Settings page
Co-authored-by: dedmamn <122568386+dedmamn@users.noreply.github.com>
---
src/pages/Settings.tsx | 80 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 79 insertions(+), 1 deletion(-)
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index 31fa60e..294fad2 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -38,11 +38,15 @@ import {
DeleteForever,
Info,
Storage,
- Backup
+ Backup,
+ Psychology,
+ History,
+ CheckCircle
} from '@mui/icons-material';
import { Settings as SettingsType, Activity, TestResult } from '../types';
import { StorageManager } from '../utils/storage';
import { DataFormatter, NotificationManager } from '../utils/helpers';
+import { formatTestDate } from '../utils/testUtils';
import PWAInstall from '../components/PWAInstall';
interface SettingsProps {
@@ -467,6 +471,80 @@ const Settings: React.FC = ({
+ {/* История тестов на усталость */}
+ {testHistory && testHistory.length > 0 && (
+
+
+
+
+ История тестов на усталость
+
+
+
+ Пройдено тестов: {testHistory.length}
+
+
+
+ {onOpenTest && (
+
+
+
+
+
+
+ )}
+
+ {testHistory.slice(0, 3).map((result, index) => (
+ onOpenTestResult?.(result)}
+ sx={{ borderRadius: 2, mb: 1 }}
+ >
+
+
+
+ 0
+ ? `Доминирующие типы: ${result.dominantTypes.slice(0, 2).map(t => t.name).join(', ')}`
+ : 'Без значимых типов усталости'
+ }
+ />
+ {result.dominantTypes.length > 0 && (
+
+ )}
+
+ ))}
+
+ {testHistory.length > 3 && (
+
+ И еще {testHistory.length - 3} результатов...
+
+ )}
+
+
+
+ )}
+
{/* PWA функции */}
From 258eae11e85b6b45bfe3738e21545b0717e5be67 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D1=8C=D0=B5=D0=B2=20=D0=94?=
=?UTF-8?q?=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?=
Date: Wed, 6 Aug 2025 11:43:57 +0300
Subject: [PATCH 4/6] Update src/components/TestResults.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/components/TestResults.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/TestResults.tsx b/src/components/TestResults.tsx
index 1d68308..944a896 100644
--- a/src/components/TestResults.tsx
+++ b/src/components/TestResults.tsx
@@ -72,7 +72,7 @@ const TestResults: React.FC = ({
type.restActivities.slice(0, 3).map(activityName => ({
name: activityName,
description: `Рекомендовано для восстановления от ${type.name.toLowerCase()}`,
- type: 'physical' as any, // Можно сделать более умный выбор типа
+ type: getActivityTypeByName(activityName),
duration: 30,
isActive: true,
createdAt: new Date(),
From 7207d664843adb1e0480bf5821ff35d2201824ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D1=8C=D0=B5=D0=B2=20=D0=94?=
=?UTF-8?q?=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?=
Date: Wed, 6 Aug 2025 11:44:27 +0300
Subject: [PATCH 5/6] Update src/components/TestModal.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/components/TestModal.tsx | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/components/TestModal.tsx b/src/components/TestModal.tsx
index 486d971..a3371fc 100644
--- a/src/components/TestModal.tsx
+++ b/src/components/TestModal.tsx
@@ -84,7 +84,15 @@ const TestModal: React.FC = ({ open, onClose, onComplete, showWa
if (allAnswered) {
onComplete(answers);
- }
+ const areAllQuestionsAnswered = (questions: TestQuestion[], answers: TestAnswer[]): boolean => {
+ return questions.every(q => answers.some(a => a.questionId === q.id));
+ };
+
+ const handleNext = () => {
+ if (currentQuestionIndex < shuffledQuestions.length - 1) {
+ setCurrentQuestionIndex(prev => prev + 1);
+ } else if (areAllQuestionsAnswered(shuffledQuestions, answers)) {
+ onComplete(answers);
}
};
From 3b281ba5445e63ab3935ce6641ef2881fd90e0bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D1=8C=D0=B5=D0=B2=20=D0=94?=
=?UTF-8?q?=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?=
Date: Wed, 6 Aug 2025 11:47:08 +0300
Subject: [PATCH 6/6] Update src/utils/testUtils.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/utils/testUtils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts
index 8578abb..d49342f 100644
--- a/src/utils/testUtils.ts
+++ b/src/utils/testUtils.ts
@@ -43,7 +43,7 @@ export const getDominantFatigueTypes = (scores: FatigueScores, maxCount: number
// Сортируем по убыванию баллов и берем только значимые (>= 11 баллов)
const significantTypes = typesWithScores
- .filter(item => item.score >= 11) // Умеренная усталость начинается с 11 баллов
+ .filter(item => item.score >= MODERATE_FATIGUE_THRESHOLD) // Умеренная усталость начинается с 11 баллов
.sort((a, b) => b.score - a.score)
.slice(0, maxCount);