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 (
+
+
+ {/* Процент в центре */}
+
+
+
+ {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 (
+
+
+ {/* Процент в центре */}
+ {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) && (
-
-
-
- )}
+
+ {/* Кнопка "Приоритеты" в правом верхнем углу */}
+ {onNavigate && (
+
+
+
+ )}
+
+ {/* Общий прогресс - большой круг в центре */}
+
+
onNavigate && onNavigate('full')}>
+
+ {/* Подсказка при наведении */}
+
+
+ Открыть статистику
+
- {onNavigate && (
-
-
-
-
- )}
-
- {sortedData.map((project, index) => {
- if (!project || !project.project_name) {
- return null
- }
-
- const projectColor = getProjectColor(project.project_name, allProjects)
-
- return (
-
- )
- })}
+ {/* Группы проектов по приоритетам */}
+
)