diff --git a/VERSION b/VERSION index d21858b..419f300 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.18.1 +3.19.0 diff --git a/play-life-web/package.json b/play-life-web/package.json index 064d8d4..c403737 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.18.1", + "version": "3.19.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/CurrentWeek.jsx b/play-life-web/src/components/CurrentWeek.jsx index aabba32..9fd4f90 100644 --- a/play-life-web/src/components/CurrentWeek.jsx +++ b/play-life-web/src/components/CurrentWeek.jsx @@ -2,10 +2,314 @@ import ProjectProgressBar from './ProjectProgressBar' import LoadingError from './LoadingError' import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils' +// Компонент круглого прогрессбара +function CircularProgressBar({ progress, size = 120, strokeWidth = 8, showCheckmark = true }) { + const normalizedProgress = Math.min(Math.max(progress || 0, 0), 100) + const radius = (size - strokeWidth) / 2 + const circumference = radius * 2 * Math.PI + const strokeDasharray = `${(normalizedProgress / 100) * circumference} ${circumference}` + + return ( +
+ + {/* Фоновая окружность */} + + {/* Прогресс окружность */} + + {/* Галочка при 100% */} + {showCheckmark && progress >= 100 && ( + + + + )} + + + + + + + + {/* Процент в центре */} +
+
+
+ {progress !== null && progress !== undefined ? `${progress.toFixed(0)}%` : 'N/A'} +
+
+
+
+ ) +} + +// Специальный компонент круглого прогрессбара с поддержкой экстра прогресса +function ProjectCircularProgressBar({ progress, extraProgress, size = 120, strokeWidth = 8, showCheckmark = true, percentage }) { + const normalizedProgress = Math.min(Math.max(progress || 0, 0), 100) + const normalizedExtraProgress = Math.min(Math.max(extraProgress || 0, 0), 100) + const radius = (size - strokeWidth) / 2 + const circumference = radius * 2 * Math.PI + const strokeDasharray = `${(normalizedProgress / 100) * circumference} ${circumference}` + const extraStrokeDasharray = normalizedExtraProgress > 0 ? `${(normalizedExtraProgress / 100) * circumference} ${circumference}` : '' + + return ( +
+ + {/* Фоновая окружность */} + + {/* Базовый прогресс окружность */} + + {/* Экстра прогресс окружность (другой цвет) */} + {normalizedExtraProgress > 0 && ( + + )} + {/* Галочка при 100% */} + {showCheckmark && progress >= 100 && ( + + + + )} + + + + + + + + + + + + {/* Процент в центре */} + {percentage && ( +
+
+
+ {percentage}% +
+
+
+ )} +
+ ) +} + +// Компонент карточки проекта с круглым прогрессбаром +function ProjectCard({ project, projectColor, onProjectClick }) { + const { project_name, total_score, min_goal_score, max_goal_score, priority } = project + + // Вычисляем прогресс по оригинальной логике из ProjectProgressBar + const getGoalProgress = () => { + const safeTotal = Number.isFinite(total_score) ? total_score : 0 + const safeMinGoal = Number.isFinite(min_goal_score) ? min_goal_score : 0 + const safeMaxGoal = Number.isFinite(max_goal_score) ? max_goal_score : 0 + + const normalizedPriority = (() => { + if (priority === null || priority === undefined) return null + const numeric = Number(priority) + return Number.isFinite(numeric) ? numeric : null + })() + + const priorityBonus = (() => { + if (normalizedPriority === 1) return 50 + if (normalizedPriority === 2) return 35 + return 20 + })() + + // Если нет валидного minGoal, возвращаем прогресс относительно maxGoal либо 0 + if (safeMinGoal <= 0) { + if (safeMaxGoal > 0) { + return Math.max(0, Math.min((safeTotal / safeMaxGoal) * 100, 100)) + } + return 0 + } + + // До достижения minGoal растем линейно от 0 до 100% + const baseProgress = Math.max(0, Math.min((safeTotal / safeMinGoal) * 100, 100)) + + // Если maxGoal не задан корректно или еще не достигнут minGoal, показываем базовый прогресс + if (safeTotal < safeMinGoal || safeMaxGoal <= safeMinGoal) { + return baseProgress + } + + // Между minGoal и maxGoal добавляем бонус в зависимости от приоритета + const extraRange = safeMaxGoal - safeMinGoal + const extraRatio = Math.min(1, Math.max(0, (safeTotal - safeMinGoal) / extraRange)) + const extraProgress = extraRatio * priorityBonus + + // Выше maxGoal прогресс не растет + return Math.min(100 + priorityBonus, 100 + extraProgress) + } + + const goalProgress = getGoalProgress() + const maxProgressForPriority = 100 + (() => { + const normalizedPriority = (() => { + if (priority === null || priority === undefined) return null + const numeric = Number(priority) + return Number.isFinite(numeric) ? numeric : null + })() + if (normalizedPriority === 1) return 50 + if (normalizedPriority === 2) return 35 + return 20 + })() + + // Для визуального отображения: 100% прогрессбара = максимум для данного приоритета + const visualProgress = (goalProgress / maxProgressForPriority) * 100 + const baseProgress = Math.min(goalProgress, 100) // Базовая часть (0-100%) + const extraProgress = Math.max(0, goalProgress - 100) // Экстра часть (свыше 100%) + const extraVisualProgress = (extraProgress / maxProgressForPriority) * 100 // Экстра часть в процентах от полного диапазона + + // Вычисляем целевую зону + const getTargetZone = () => { + const safeMinGoal = Number.isFinite(min_goal_score) ? min_goal_score : 0 + const safeMaxGoal = Number.isFinite(max_goal_score) ? max_goal_score : 0 + + if (safeMinGoal > 0 && safeMaxGoal > 0) { + return `${safeMinGoal.toFixed(0)} - ${safeMaxGoal.toFixed(0)}` + } else if (safeMinGoal > 0) { + return `${safeMinGoal.toFixed(0)}+` + } + return '0+' + } + + const handleClick = () => { + if (onProjectClick) { + onProjectClick(project_name) + } + } + + return ( +
+ {/* Верхняя часть с названием и прогрессом */} +
+ {/* Левая часть - текст (название, баллы, целевая зона) */} +
+
+ {project_name} +
+
+ {total_score?.toFixed(1) || '0.0'} +
+
+ Целевая зона: {getTargetZone()} +
+
+ + {/* Правая часть - круглый прогрессбар */} +
+ +
+
+
+ ) +} + +// Компонент группы проектов по приоритету +function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick }) { + if (projects.length === 0) return null + + return ( +
+ {/* Заголовок группы */} +
+

{title}

+ + {subtitle} +
+ + {/* Карточки проектов */} +
+ {projects.map((project, index) => { + if (!project || !project.project_name) return null + + const projectColor = getProjectColor(project.project_name, allProjects) + + return ( + + ) + })} +
+
+ ) +} + function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProjectsData, onNavigate }) { // Обрабатываем данные: может быть объект с projects и total, или просто массив const projectsData = data?.projects || (Array.isArray(data) ? data : []) || [] - + // Показываем loading только если данных нет и идет загрузка if (loading && (!data || projectsData.length === 0)) { return ( @@ -27,14 +331,14 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject // Проверяем различные возможные названия поля const rawValue = data?.total ?? data?.progress ?? data?.percentage ?? data?.completion ?? data?.goal_progress const parsedValue = rawValue === undefined || rawValue === null ? null : parseFloat(rawValue) - + if (Number.isFinite(parsedValue) && parsedValue >= 0) { return Math.max(0, parsedValue) // Убрали ограничение на 100, так как может быть больше } - + return null // null означает, что данные не пришли })() - + const hasProgressData = overallProgress !== null // Получаем отсортированный список всех проектов для синхронизации цветов @@ -46,130 +350,137 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject return Number.isFinite(numeric) ? numeric : Infinity } - // Сортируем: сначала по priority (1, 2, ...; null в конце), затем по min_goal_score по убыванию - const sortedData = (projectsData && projectsData.length > 0) ? [...projectsData].sort((a, b) => { - const priorityA = normalizePriority(a.priority) - const priorityB = normalizePriority(b.priority) + // Группируем проекты по приоритетам + const priorityGroups = { + main: [], // priority === 1 + important: [], // priority === 2 + others: [] // остальные + } - if (priorityA !== priorityB) { - return priorityA - priorityB - } + if (projectsData && projectsData.length > 0) { + projectsData.forEach(project => { + if (!project || !project.project_name) return - const minGoalA = parseFloat(a.min_goal_score) || 0 - const minGoalB = parseFloat(b.min_goal_score) || 0 - return minGoalB - minGoalA - }) : [] + const priority = normalizePriority(project.priority) + if (priority === 1) { + priorityGroups.main.push(project) + } else if (priority === 2) { + priorityGroups.important.push(project) + } else { + priorityGroups.others.push(project) + } + }) + + // Сортируем внутри каждой группы по min_goal_score по убыванию + Object.values(priorityGroups).forEach(group => { + group.sort((a, b) => { + const minGoalA = parseFloat(a.min_goal_score) || 0 + const minGoalB = parseFloat(b.min_goal_score) || 0 + return minGoalB - minGoalA + }) + }) + } + + // Вычисляем процент выполнения для каждой группы + const calculateGroupProgress = (projects) => { + if (projects.length === 0) return 0 + + let totalProgress = 0 + let validProjects = 0 + + projects.forEach(project => { + const safeTotal = Number.isFinite(project.total_score) ? project.total_score : 0 + const safeMinGoal = Number.isFinite(project.min_goal_score) ? project.min_goal_score : 0 + + if (safeMinGoal > 0) { + const projectProgress = Math.min((safeTotal / safeMinGoal) * 100, 100) + totalProgress += projectProgress + validProjects++ + } + }) + + return validProjects > 0 ? totalProgress / validProjects : 0 + } + + const mainProgress = calculateGroupProgress(priorityGroups.main) + const importantProgress = calculateGroupProgress(priorityGroups.important) + const othersProgress = calculateGroupProgress(priorityGroups.others) + + // Пересчитываем общий прогресс как среднее от групповых процентов + const recalculatedOverallProgress = (() => { + const groups = [] + if (priorityGroups.main.length > 0) groups.push(mainProgress) + if (priorityGroups.important.length > 0) groups.push(importantProgress) + if (priorityGroups.others.length > 0) groups.push(othersProgress) + + if (groups.length === 0) return null + + const average = groups.reduce((sum, progress) => sum + progress, 0) / groups.length + return Math.max(0, Math.min(average, 100)) // Ограничиваем 0-100% + })() + + // Используем пересчитанный общий прогресс вместо API данных + const displayOverallProgress = recalculatedOverallProgress !== null ? recalculatedOverallProgress : (hasProgressData ? overallProgress : null) return ( -
- {/* Информация об общем проценте выполнения целей */} -
-
-
-
-
Выполнение целей
-
- {hasProgressData && typeof overallProgress === 'number' && Number.isFinite(overallProgress) - ? `${overallProgress.toFixed(1)}%` - : 'N/A'} -
-
- {hasProgressData && typeof overallProgress === 'number' && Number.isFinite(overallProgress) && ( -
- - - - {overallProgress >= 100 && ( - - - - )} - - - - - - - -
- )} +
+ {/* Кнопка "Приоритеты" в правом верхнем углу */} + {onNavigate && ( +
+ +
+ )} + + {/* Общий прогресс - большой круг в центре */} +
+
onNavigate && onNavigate('full')}> + + {/* Подсказка при наведении */} +
+ + Открыть статистику +
- {onNavigate && ( -
- - -
- )}
-
- {sortedData.map((project, index) => { - if (!project || !project.project_name) { - return null - } - - const projectColor = getProjectColor(project.project_name, allProjects) - - return ( -
- -
- ) - })} + {/* Группы проектов по приоритетам */} +
+ + + + +
)