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 ( + + + + + + + Добро пожаловать! + + + + + + Для персонализированных рекомендаций по отдыху рекомендуем пройти + короткий тест на определение типов усталости. + + + + + + + + Что даст тест? + + + + • Определение ваших доминирующих типов усталости
+ • Персональные рекомендации по восстановлению
+ • Готовые активности для календаря отдыха
+ • Понимание причин вашей усталости +
+
+
+ + + + Время: 5-10 минут + + + Вопросов: 40 + + + Можно пересдать: в любое время + + +
+ + + + + +
+ ); +}; + +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 ( + + + + + Тест на усталость + + + + + + + + + {testInstructions.title} + + + + {testInstructions.instructions} + + + + {testInstructions.warning} + + + + + Количество вопросов: {testQuestions.length} + + + + Примерное время: 5-10 мин + + + + + + + + + + ); + } + + return ( + + + + + + Тест на усталость + + + Вопрос {currentQuestionIndex + 1} из {shuffledQuestions.length} + + + + + + + + + + + + Прогресс: {Math.round(progress)}% + + + Отвечено: {answeredCount}/{shuffledQuestions.length} + + + + + {hasShownWarning && answers.length > 0 && ( + + Вы уверены, что хотите прервать тест? Для получения результатов необходимо ответить на все вопросы. + + )} + + + {currentQuestion && ( + + + + + + + {currentQuestion.text} + + + + handleAnswerChange(currentQuestion.id, parseInt(e.target.value))} + > + {[0, 1, 2, 3, 4, 5].map((score) => ( + } + label={ + + {score} - { + score === 0 ? 'Никогда / почти никогда' : + score === 1 ? 'Редко (1-2 раза)' : + score === 2 ? 'Иногда (3-4 раза)' : + score === 3 ? 'Часто' : + score === 4 ? 'Очень часто' : + 'Постоянно' + } + + } + sx={{ + '& .MuiFormControlLabel-label': { + fontSize: '0.875rem' + } + }} + /> + ))} + + + + )} + + + + + + + + {hasShownWarning && ( + <> + + + + )} + + {!hasShownWarning && ( + + )} + + + ); +}; + +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 ( + + + + + + Результаты теста + + + {formatTestDate(completedAt)} + + + + + + + + + {/* Медицинское предупреждение */} + {shouldShowMedicalWarning(dominantTypes) && ( + } + sx={{ mb: 3 }} + > + + Важно: Для СХУ и гормональной усталости с высокими баллами (>15) требуется консультация врача. + + + )} + + {/* Основные результаты */} + + + + Ваши доминирующие типы усталости + + + {dominantTypes.length === 0 ? ( + + + 🎉 Отличные новости! У вас не выявлено значимых признаков усталости. + Продолжайте поддерживать здоровый образ жизни и регулярный отдых. + + + ) : ( + + {dominantTypes.map((type, index) => ( + + + + + {index + 1}. {type.name} + + + + {getScoreInterpretation(type.score)} + + + + + + + ))} + + )} + + + {/* Подробные описания и рекомендации */} + {dominantTypes.length > 0 && ( + + + + Подробные рекомендации + + + {dominantTypes.map((type, index) => ( + + } + sx={{ + backgroundColor: index === 0 ? 'primary.light' : 'background.default', + '& .MuiAccordionSummary-content': { + alignItems: 'center' + } + }} + > + + {type.name} + + + + + + + + {type.description} + + + + + + {type.solutions} + + + + + + Рекомендованные виды отдыха: + + + {type.restActivities.map((activity, actIndex) => ( + + + + + + + ))} + + + + + ))} + + )} + + {/* Все результаты */} + + + Полные результаты по всем типам + + + + {Object.entries(fatigueScores).map(([type, score]) => ( + + + {type} + + + + ))} + + + + + + + + {onAddActivitiesToCalendar && dominantTypes.length > 0 && ( + + )} + + + + + + + ); +}; + +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);