diff --git a/VERSION b/VERSION index 0216ba3..a162ea7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.10.2 +4.11.0 diff --git a/play-life-web/package.json b/play-life-web/package.json index 3cfda4c..23fea55 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "4.10.2", + "version": "4.11.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index d1ad829..237ad2e 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -550,11 +550,9 @@ function AppContent() { setActiveTab(tab) setTabParams(params) markTabAsLoaded(tab) - // Если это экран full с selectedProject, восстанавливаем его - if (tab === 'full' && params.selectedProject) { - setSelectedProject(params.selectedProject) - } else if (tab === 'full') { - setSelectedProject(null) + // Если это экран full, устанавливаем selectedProject только если он есть в params + if (tab === 'full') { + setSelectedProject(params.selectedProject || null) } return } @@ -580,9 +578,9 @@ function AppContent() { } }) setTabParams(params) - // Если это экран full с selectedProject, восстанавливаем его - if (tabFromUrl === 'full' && params.selectedProject) { - setSelectedProject(params.selectedProject) + // Если это экран full, устанавливаем selectedProject только если он есть в params + if (tabFromUrl === 'full') { + setSelectedProject(params.selectedProject || null) } } else { // Если в URL нет глубокого таба, значит мы вернулись на основной таб @@ -697,6 +695,11 @@ function AppContent() { setActiveTab(tab) if (tab === 'current') { setSelectedProject(null) + } else if (tab === 'full') { + // Если переходим на full без selectedProject в params, очищаем выбранный проект + if (!params.selectedProject) { + setSelectedProject(null) + } } // Обновляем список слов при возврате из экрана добавления слов if (activeTab === 'add-words' && tab === 'words') { diff --git a/play-life-web/src/components/FullStatistics.jsx b/play-life-web/src/components/FullStatistics.jsx index a67bcd3..fbc3444 100644 --- a/play-life-web/src/components/FullStatistics.jsx +++ b/play-life-web/src/components/FullStatistics.jsx @@ -1,187 +1,18 @@ import React from 'react' -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - Filler -} from 'chart.js' -import { Line } from 'react-chartjs-2' import WeekProgressChart from './WeekProgressChart' import LoadingError from './LoadingError' -import { getAllProjectsSorted, getProjectColor, sortProjectsLikeCurrentWeek } from '../utils/projectUtils' +import { getAllProjectsSorted } from '../utils/projectUtils' import './Integrations.css' // Экспортируем для обратной совместимости (если используется в других местах) export { getProjectColorByIndex } from '../utils/projectUtils' -const parseWeekKey = (weekKey) => { - const [yearStr, weekStr] = weekKey.split('-W') - return { year: Number(yearStr), week: Number(weekStr) } -} - -const compareWeekKeys = (a, b) => { - const [yearA, weekA] = a.split('-W').map(Number) - const [yearB, weekB] = b.split('-W').map(Number) - - if (yearA !== yearB) { - return yearA - yearB - } - return weekA - weekB -} - -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - Filler -) - function FullStatistics({ selectedProject, onClearSelection, data, loading, error, onRetry, currentWeekData, onNavigate }) { - const processData = () => { - if (!data || data.length === 0) return null - - // Группируем данные по проектам - const projectsMap = {} - - data.forEach(item => { - const projectName = item.project_name - const weekKey = `${item.report_year}-W${item.report_week.toString().padStart(2, '0')}` - - if (!projectsMap[projectName]) { - projectsMap[projectName] = {} - } - - projectsMap[projectName][weekKey] = parseFloat(item.total_score) - }) - - // Собираем все уникальные недели и сортируем их по году и неделе - const allWeeks = new Set() - Object.values(projectsMap).forEach(weeks => { - Object.keys(weeks).forEach(week => allWeeks.add(week)) - }) - const sortedWeeks = Array.from(allWeeks).sort(compareWeekKeys) - - // Получаем отсортированный список всех проектов для синхронизации цветов - const allProjectNames = getAllProjectsSorted(data) - - // Фильтруем по выбранному проекту, если он указан - let projectNames = allProjectNames.filter(name => projectsMap[name]) - - // Сортируем проекты так же, как на экране списка проектов (по priority и min_goal_score) - if (currentWeekData) { - projectNames = sortProjectsLikeCurrentWeek(projectNames, currentWeekData) - } - - if (selectedProject) { - projectNames = projectNames.filter(name => name === selectedProject) - } - - const datasets = projectNames.map((projectName) => { - let cumulativeSum = 0 - const values = sortedWeeks.map(week => { - const score = projectsMap[projectName]?.[week] || 0 - cumulativeSum += score - return cumulativeSum - }) - - // Генерируем цвет для линии на основе индекса в полном списке проектов - const color = getProjectColor(projectName, allProjectNames) - - return { - label: projectName, - data: values, - borderColor: color, - backgroundColor: color.replace('50%)', '20%)'), - fill: false, - tension: 0.4, - pointRadius: 4, - pointHoverRadius: 6, - } - }) - - return { - labels: sortedWeeks, - datasets: datasets, - } - } - - const chartData = processData() - - if (error && !chartData && !loading) { + if (error && (!data || data.length === 0) && !loading) { return } - const chartOptions = { - responsive: true, - maintainAspectRatio: false, - animation: false, - plugins: { - legend: { - position: 'bottom', - labels: { - usePointStyle: true, - padding: 15, - font: { - size: 12, - }, - }, - padding: { - top: 20, - }, - }, - title: { - display: false, - }, - tooltip: { - mode: 'index', - intersect: false, - callbacks: { - label: function(context) { - return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}` - } - } - }, - }, - scales: { - x: { - display: true, - title: { - display: false, - }, - grid: { - display: true, - color: 'rgba(0, 0, 0, 0.05)', - }, - }, - y: { - display: true, - title: { - display: false, - }, - beginAtZero: true, - grid: { - display: true, - color: 'rgba(0, 0, 0, 0.05)', - }, - }, - }, - interaction: { - mode: 'nearest', - axis: 'x', - intersect: false, - }, - } - return (
@@ -194,24 +25,19 @@ function FullStatistics({ selectedProject, onClearSelection, data, loading, erro ✕ )} - {loading && !chartData ? ( + {loading && (!data || data.length === 0) ? (
Загрузка...
- ) : !chartData ? ( + ) : (!data || data.length === 0) ? (
Нет данных для отображения
) : ( - <> -
- -
- - + )}
) diff --git a/play-life-web/src/components/WeekProgressChart.jsx b/play-life-web/src/components/WeekProgressChart.jsx index d58b8ff..63a9aaa 100644 --- a/play-life-web/src/components/WeekProgressChart.jsx +++ b/play-life-web/src/components/WeekProgressChart.jsx @@ -18,40 +18,44 @@ const compareWeekKeys = (a, b) => { return weekA - weekB } +// Функция для определения текущей недели в ISO формате +// ISO 8601: неделя начинается с понедельника, первая неделя года - та, в которой есть 4 января +const getCurrentWeek = () => { + const now = new Date() + const date = new Date(now.getTime()) + + // ISO week calculation + // Set to nearest Thursday: current date + 4 - current day number + // Make Sunday's day number 7 + const day = date.getDay() || 7 + date.setDate(date.getDate() + 4 - day) + + // Get first day of year + const yearStart = new Date(date.getFullYear(), 0, 1) + + // Calculate full weeks to nearest Thursday + const week = Math.ceil((((date - yearStart) / 86400000) + 1) / 7) + + // ISO year is the year of the Thursday of that week + const year = date.getFullYear() + + return { year, week } +} + function WeekProgressChart({ data, allProjectsSorted, currentWeekData, selectedProject }) { - if (!data || data.length === 0) { - return null - } - - // Группируем данные по неделям - const weeksMap = {} - - data.forEach(item => { - // Фильтруем по выбранному проекту, если он указан - if (selectedProject && item.project_name !== selectedProject) { - return - } - - const weekKey = `${item.report_year}-W${item.report_week.toString().padStart(2, '0')}` - - if (!weeksMap[weekKey]) { - weeksMap[weekKey] = [] - } - - weeksMap[weekKey].push({ - projectName: item.project_name, - score: parseFloat(item.total_score) || 0 - }) - }) - - // Получаем все уникальные недели и сортируем их (новые сверху) - const allWeeks = Object.keys(weeksMap).sort((a, b) => -compareWeekKeys(a, b)) - - // Берем первые 4 недели (самые актуальные) - const last4Weeks = allWeeks.slice(0, 4) + // Определяем текущую неделю + const currentWeek = getCurrentWeek() + const currentWeekKey = formatWeekKey(currentWeek) // Используем переданный отсортированный список проектов или получаем из данных const allProjects = allProjectsSorted || (() => { + if (!data || data.length === 0) { + // Если нет данных, используем проекты из currentWeekData + if (currentWeekData && currentWeekData.projects) { + return currentWeekData.projects.map(p => p.project_name || p.name).filter(Boolean) + } + return [] + } const allProjectsSet = new Set() data.forEach(item => { allProjectsSet.add(item.project_name) @@ -59,9 +63,91 @@ function WeekProgressChart({ data, allProjectsSorted, currentWeekData, selectedP return Array.from(allProjectsSet).sort() })() - // Обрабатываем данные для каждой недели - const weeksData = last4Weeks.map(weekKey => { - const weekProjects = weeksMap[weekKey] + // Группируем данные по неделям + const weeksMap = {} + + if (data && data.length > 0) { + data.forEach(item => { + // Фильтруем по выбранному проекту, если он указан + if (selectedProject && item.project_name !== selectedProject) { + return + } + + const weekKey = `${item.report_year}-W${item.report_week.toString().padStart(2, '0')}` + + if (!weeksMap[weekKey]) { + weeksMap[weekKey] = [] + } + + weeksMap[weekKey].push({ + projectName: item.project_name, + score: parseFloat(item.total_score) || 0 + }) + }) + } + + // Добавляем данные текущей недели из currentWeekData, если они отсутствуют в data + if (currentWeekData && currentWeekData.projects && !weeksMap[currentWeekKey]) { + const projects = Array.isArray(currentWeekData.projects) + ? currentWeekData.projects + : (currentWeekData.projects?.projects || []) + + weeksMap[currentWeekKey] = projects.map(project => ({ + projectName: project.project_name || project.name, + score: parseFloat(project.total_score || project.score || 0) + })).filter(p => { + // Фильтруем по выбранному проекту, если он указан + if (selectedProject && p.projectName !== selectedProject) { + return false + } + return true + }) + } + + // Получаем все уникальные недели и сортируем их (новые сверху) + const allWeeks = Object.keys(weeksMap).sort((a, b) => -compareWeekKeys(a, b)) + + // Находим индекс текущей недели + const currentWeekIndex = allWeeks.findIndex(w => w === currentWeekKey) + + // Разделяем недели на группы + // allWeeks отсортированы от новых к старым (индекс 0 - самая новая) + // Если currentWeekIndex = 2: + // - allWeeks[0, 1] - более новые недели (будущие, если есть) + // - allWeeks[2] - текущая неделя + // - allWeeks[3, 4, ...] - более старые недели (прошлые) + let last4Weeks = [] // Последние 4 недели ДО текущей (более старые) + let next4Weeks = [] // Следующие 4 недели ПОСЛЕ текущей (более новые, если есть) + let currentWeekInData = null + + if (currentWeekIndex >= 0) { + // Текущая неделя найдена в данных + // Берем 4 недели ДО текущей (более старые) - это индексы после currentWeekIndex + last4Weeks = allWeeks.slice(currentWeekIndex + 1, currentWeekIndex + 5) + // Берем 4 недели ПОСЛЕ текущей (более новые) - это индексы до currentWeekIndex + next4Weeks = allWeeks.slice(Math.max(0, currentWeekIndex - 4), currentWeekIndex) + // Текущая неделя + currentWeekInData = currentWeekKey + } else { + // Текущая неделя не найдена в данных, но мы её добавили из currentWeekData + if (weeksMap[currentWeekKey]) { + // Добавляем текущую неделю в начало списка (она самая новая) + const weeksWithCurrent = [currentWeekKey, ...allWeeks] + // Последние 4 недели - это первые 4 из старых данных (более старые) + last4Weeks = allWeeks.slice(0, 4) + // Следующие недели после текущей - их нет, так как текущая самая новая + next4Weeks = [] + currentWeekInData = currentWeekKey + } else { + // Если текущей недели нет вообще, просто берем последние 4 (самые новые из доступных) + last4Weeks = allWeeks.slice(0, 4) + next4Weeks = [] + } + } + + // Функция для обработки данных недели + const processWeekData = (weekKey) => { + const weekProjects = weeksMap[weekKey] || [] const totalScore = weekProjects.reduce((sum, p) => sum + p.score, 0) // Используем абсолютные значения (баллы) @@ -93,64 +179,99 @@ function WeekProgressChart({ data, allProjectsSorted, currentWeekData, selectedP year, week, projects: sortedProjectsWithData, - totalScore + totalScore, + isCurrent: weekKey === currentWeekKey } - }) + } + // Обрабатываем данные для всех недель + const currentWeekDataProcessed = currentWeekInData ? processWeekData(currentWeekInData) : null + + // Объединяем все недели кроме текущей + // next4Weeks уже отсортированы от новых к старым (индексы 0..currentWeekIndex-1) + // last4Weeks уже отсортированы от новых к старым (индексы currentWeekIndex+1..) + // Для отображения: старые сверху, новые снизу - нужно перевернуть порядок + // Объединяем: сначала более старые (last4Weeks), потом более новые (next4Weeks) + const allOtherWeeks = [...last4Weeks, ...next4Weeks] + // Переворачиваем, чтобы старые были сверху, новые снизу + const allOtherWeeksData = allOtherWeeks.map(processWeekData).reverse() + + // Объединяем все недели для расчета максимального значения + const allWeeksData = [...(currentWeekDataProcessed ? [currentWeekDataProcessed] : []), ...allOtherWeeksData] + // Находим максимальное значение среди всех недель для единой шкалы сравнения - const maxTotalScore = Math.max(...weeksData.map(w => w.totalScore), 1) + const maxTotalScore = Math.max(...allWeeksData.map(w => w.totalScore), 1) - if (weeksData.length === 0) { + if (allWeeksData.length === 0) { return null } + // Компонент для отображения прогрессбара недели + const WeekProgressBar = ({ weekData }) => ( +
+
+ Неделя {weekData.week} +
+
+ {weekData.totalScore === 0 ? ( +
+ Нет данных +
+ ) : ( + <> + {weekData.projects.map((project, index) => { + // Вычисляем позицию и ширину для каждого сегмента на основе абсолютных значений + let left = 0 + for (let i = 0; i < index; i++) { + left += weekData.projects[i].score + } + + const widthPercent = (project.score / maxTotalScore) * 100 + const leftPercent = (left / maxTotalScore) * 100 + + return ( +
+ ) + })} + + )} +
+
+ {weekData.totalScore > 0 ? `${weekData.totalScore.toFixed(1)}` : '-'} +
+
+ ) + return (
-

Последние 4 недели

+

Прогресс недель

- {weeksData.map((weekData) => ( -
-
- Неделя {weekData.week} -
-
- {weekData.totalScore === 0 ? ( -
- Нет данных -
- ) : ( - <> - {weekData.projects.map((project, index) => { - // Вычисляем позицию и ширину для каждого сегмента на основе абсолютных значений - let left = 0 - for (let i = 0; i < index; i++) { - left += weekData.projects[i].score - } - - const widthPercent = (project.score / maxTotalScore) * 100 - const leftPercent = (left / maxTotalScore) * 100 - - return ( -
- ) - })} - - )} -
-
- {weekData.totalScore > 0 ? `${weekData.totalScore.toFixed(1)}` : '-'} -
-
+ {/* Остальные недели (старые сверху, новые снизу) */} + {allOtherWeeksData.map((weekData) => ( + ))} + + {/* Разделитель перед текущей неделей */} + {currentWeekDataProcessed && ( +
+ )} + + {/* Текущая неделя (последняя) */} + {currentWeekDataProcessed && ( + <> +

Текущая неделя

+ + + )}
)