import ProjectProgressBar from './ProjectProgressBar' import LoadingError from './LoadingError' import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils' import { CircularProgressbar, buildStyles } from 'react-circular-progressbar' import 'react-circular-progressbar/dist/styles.css' // Компонент круглого прогрессбара с использованием react-circular-progressbar function CircularProgressBar({ progress, size = 120, strokeWidth = 8, showCheckmark = true, extraProgress = null, maxProgress = 100, textSize = 'large', displayProgress = null, textPosition = 'default', projectColor = null }) { // Нормализуем прогресс для визуализации (0-100%) const normalizedProgress = Math.min(Math.max(progress || 0, 0), 100) // Если есть extra progress, вычисляем визуальный прогресс для overlay const extraVisual = extraProgress !== null && extraProgress > 0 ? Math.min((extraProgress / maxProgress) * 100, 100) : 0 // Определяем, достигнут ли 100% или выше const isComplete = (displayProgress !== null ? displayProgress : progress) >= 100 // Определяем градиент ID: зелёный если >= 100%, иначе по наличию extra progress const gradientId = isComplete ? 'success-gradient' : (extraVisual > 0 ? 'project-gradient' : 'overall-gradient') const extraGradientId = 'project-extra-gradient' // Определяем класс размера текста const textSizeClass = textSize === 'large' ? 'text-4xl' : textSize === 'small' ? 'text-base' : 'text-lg' // Используем displayProgress если передан (может быть больше 100%), иначе progress const progressToDisplay = displayProgress !== null ? displayProgress : progress return (
{/* Extra progress overlay (если есть) */} {extraVisual > 0 && ( )} {/* Иконка статистики в центре */}
{/* Кастомный текст снизу */}
{progressToDisplay !== null && progressToDisplay !== undefined ? `${progressToDisplay.toFixed(0)}%` : 'N/A'}
{/* Градиенты для SVG */}
) } // Компонент карточки проекта с круглым прогрессбаром 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% прогрессбара = максимум для данного приоритета // visualProgress показывает процент заполнения прогрессбара (0-100%), где 100% = maxProgressForPriority const visualProgress = Math.min((goalProgress / maxProgressForPriority) * 100, 100) // Для extra overlay: если goalProgress > 100%, показываем extra часть // Но визуально это уже учтено в visualProgress, так что extra overlay не нужен // Однако если нужно показать, что достигнут максимум, можно использовать другой подход const baseVisualProgress = visualProgress const extraVisualProgress = 0 // Не используем extra overlay, так как visualProgress уже показывает весь прогресс // Вычисляем целевую зону 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 (
Загрузка...
) } if (error && (!data || projectsData.length === 0)) { return } // Процент выполнения берем только из данных API const overallProgress = (() => { // Проверяем различные возможные названия поля 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 // Получаем отсортированный список всех проектов для синхронизации цветов const allProjects = getAllProjectsSorted(allProjectsData, projectsData || []) const normalizePriority = (value) => { if (value === null || value === undefined) return Infinity const numeric = Number(value) return Number.isFinite(numeric) ? numeric : Infinity } // Группируем проекты по приоритетам const priorityGroups = { main: [], // priority === 1 important: [], // priority === 2 others: [] // остальные } if (projectsData && projectsData.length > 0) { projectsData.forEach(project => { if (!project || !project.project_name) return 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, average) // Убираем ограничение на 100% для текста })() // Используем пересчитанный общий прогресс вместо API данных const displayOverallProgress = recalculatedOverallProgress !== null ? recalculatedOverallProgress : (hasProgressData ? overallProgress : null) return (
{/* Кнопка "Приоритеты" в правом верхнем углу */} {onNavigate && (
)} {/* Общий прогресс - большой круг в центре */}
onNavigate && onNavigate('full')}> {/* Подсказка при наведении */}
Открыть статистику
{/* Группы проектов по приоритетам */}
) } export default CurrentWeek