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 && (
+ <>
+
Текущая неделя
+
+ >
+ )}
)