2025-12-29 20:01:55 +03:00
|
|
|
|
import ProjectProgressBar from './ProjectProgressBar'
|
2026-01-11 15:51:28 +03:00
|
|
|
|
import LoadingError from './LoadingError'
|
2025-12-29 20:01:55 +03:00
|
|
|
|
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
|
|
|
|
|
|
|
|
|
|
|
function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProjectsData, onNavigate }) {
|
|
|
|
|
|
// Обрабатываем данные: может быть объект с projects и total, или просто массив
|
2026-01-02 16:29:58 +03:00
|
|
|
|
const projectsData = data?.projects || (Array.isArray(data) ? data : []) || []
|
2025-12-29 20:01:55 +03:00
|
|
|
|
|
|
|
|
|
|
// Показываем loading только если данных нет и идет загрузка
|
|
|
|
|
|
if (loading && (!data || projectsData.length === 0)) {
|
|
|
|
|
|
return (
|
2026-01-11 15:32:31 +03:00
|
|
|
|
<div className="fixed inset-0 bottom-20 flex justify-center items-center">
|
2025-12-29 20:01:55 +03:00
|
|
|
|
<div className="flex flex-col items-center">
|
|
|
|
|
|
<div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
|
2026-01-11 15:32:31 +03:00
|
|
|
|
<div className="text-gray-600 font-medium">Загрузка...</div>
|
2025-12-29 20:01:55 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (error && (!data || projectsData.length === 0)) {
|
2026-01-11 15:51:28 +03:00
|
|
|
|
return <LoadingError onRetry={onRetry} />
|
2025-12-29 20:01:55 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Процент выполнения берем только из данных 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
|
|
|
|
|
|
|
|
|
|
|
|
// Логирование для отладки
|
|
|
|
|
|
console.log('CurrentWeek data:', {
|
|
|
|
|
|
data,
|
|
|
|
|
|
dataTotal: data?.total,
|
|
|
|
|
|
dataProgress: data?.progress,
|
|
|
|
|
|
dataPercentage: data?.percentage,
|
|
|
|
|
|
overallProgress,
|
|
|
|
|
|
hasProgressData
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Получаем отсортированный список всех проектов для синхронизации цветов
|
2026-01-02 16:29:58 +03:00
|
|
|
|
const allProjects = getAllProjectsSorted(allProjectsData, projectsData || [])
|
2025-12-29 20:01:55 +03:00
|
|
|
|
|
|
|
|
|
|
const normalizePriority = (value) => {
|
|
|
|
|
|
if (value === null || value === undefined) return Infinity
|
|
|
|
|
|
const numeric = Number(value)
|
|
|
|
|
|
return Number.isFinite(numeric) ? numeric : Infinity
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Сортируем: сначала по priority (1, 2, ...; null в конце), затем по min_goal_score по убыванию
|
2026-01-02 16:29:58 +03:00
|
|
|
|
const sortedData = (projectsData && projectsData.length > 0) ? [...projectsData].sort((a, b) => {
|
2025-12-29 20:01:55 +03:00
|
|
|
|
const priorityA = normalizePriority(a.priority)
|
|
|
|
|
|
const priorityB = normalizePriority(b.priority)
|
|
|
|
|
|
|
|
|
|
|
|
if (priorityA !== priorityB) {
|
|
|
|
|
|
return priorityA - priorityB
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const minGoalA = parseFloat(a.min_goal_score) || 0
|
|
|
|
|
|
const minGoalB = parseFloat(b.min_goal_score) || 0
|
|
|
|
|
|
return minGoalB - minGoalA
|
2026-01-02 16:29:58 +03:00
|
|
|
|
}) : []
|
2025-12-29 20:01:55 +03:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{/* Информация об общем проценте выполнения целей */}
|
|
|
|
|
|
<div className="mb-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg p-4 border border-indigo-200">
|
|
|
|
|
|
<div className="flex items-stretch justify-between gap-4">
|
|
|
|
|
|
<div className="min-w-0 flex-1 flex items-center gap-4">
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<div className="text-sm sm:text-base text-gray-600 mb-1">Выполнение целей</div>
|
|
|
|
|
|
<div className="text-2xl sm:text-3xl lg:text-4xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
2026-01-02 16:29:58 +03:00
|
|
|
|
{hasProgressData && typeof overallProgress === 'number' && Number.isFinite(overallProgress)
|
|
|
|
|
|
? `${overallProgress.toFixed(1)}%`
|
|
|
|
|
|
: 'N/A'}
|
2025-12-29 20:01:55 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-02 16:29:58 +03:00
|
|
|
|
{hasProgressData && typeof overallProgress === 'number' && Number.isFinite(overallProgress) && (
|
2025-12-29 20:01:55 +03:00
|
|
|
|
<div className="w-12 h-12 sm:w-16 sm:h-16 relative flex-shrink-0">
|
|
|
|
|
|
<svg className="transform -rotate-90" viewBox="0 0 64 64">
|
|
|
|
|
|
<circle
|
|
|
|
|
|
cx="32"
|
|
|
|
|
|
cy="32"
|
|
|
|
|
|
r="28"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="6"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
className="text-gray-200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<circle
|
|
|
|
|
|
cx="32"
|
|
|
|
|
|
cy="32"
|
|
|
|
|
|
r="28"
|
|
|
|
|
|
stroke="url(#gradient)"
|
|
|
|
|
|
strokeWidth="6"
|
|
|
|
|
|
fill="none"
|
2026-01-02 16:29:58 +03:00
|
|
|
|
strokeDasharray={`${Math.min(Math.max(overallProgress / 100, 0), 1) * 175.93} 175.93`}
|
2025-12-29 20:01:55 +03:00
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{overallProgress >= 100 && (
|
|
|
|
|
|
<g className="transform rotate-90" style={{ transformOrigin: '32px 32px' }}>
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M 20 32 L 28 40 L 44 24"
|
|
|
|
|
|
stroke="url(#gradient)"
|
|
|
|
|
|
strokeWidth="4"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<defs>
|
|
|
|
|
|
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
|
|
|
|
<stop offset="0%" stopColor="#4f46e5" />
|
|
|
|
|
|
<stop offset="100%" stopColor="#9333ea" />
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
</defs>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{onNavigate && (
|
|
|
|
|
|
<div className="flex flex-col flex-shrink-0 gap-1" style={{ minHeight: '64px', height: '100%' }}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onNavigate('full')}
|
|
|
|
|
|
className="flex-1 flex items-center justify-center px-4 bg-white hover:bg-indigo-50 text-indigo-600 hover:text-indigo-700 rounded-lg border border-indigo-200 hover:border-indigo-300 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
|
|
|
|
title="Статистика"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
|
<line x1="18" y1="20" x2="18" y2="10"></line>
|
|
|
|
|
|
<line x1="12" y1="20" x2="12" y2="4"></line>
|
|
|
|
|
|
<line x1="6" y1="20" x2="6" y2="14"></line>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onNavigate('priorities')}
|
|
|
|
|
|
className="flex-1 flex items-center justify-center px-4 bg-white hover:bg-indigo-50 text-indigo-600 hover:text-indigo-700 rounded-lg border border-indigo-200 hover:border-indigo-300 transition-all duration-200 shadow-sm hover:shadow-md"
|
2025-12-29 21:31:43 +03:00
|
|
|
|
title="Проекты"
|
2025-12-29 20:01:55 +03:00
|
|
|
|
>
|
|
|
|
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
2025-12-29 21:31:43 +03:00
|
|
|
|
<rect x="3" y="3" width="7" height="7"></rect>
|
|
|
|
|
|
<rect x="14" y="3" width="7" height="7"></rect>
|
|
|
|
|
|
<rect x="14" y="14" width="7" height="7"></rect>
|
|
|
|
|
|
<rect x="3" y="14" width="7" height="7"></rect>
|
2025-12-29 20:01:55 +03:00
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
|
|
|
|
|
{sortedData.map((project, index) => {
|
2026-01-02 16:29:58 +03:00
|
|
|
|
if (!project || !project.project_name) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 20:01:55 +03:00
|
|
|
|
const projectColor = getProjectColor(project.project_name, allProjects)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={index}>
|
|
|
|
|
|
<ProjectProgressBar
|
|
|
|
|
|
projectName={project.project_name}
|
2026-01-02 16:29:58 +03:00
|
|
|
|
totalScore={parseFloat(project.total_score) || 0}
|
|
|
|
|
|
minGoalScore={parseFloat(project.min_goal_score) || 0}
|
|
|
|
|
|
maxGoalScore={parseFloat(project.max_goal_score) || 0}
|
2025-12-29 20:01:55 +03:00
|
|
|
|
onProjectClick={onProjectClick}
|
|
|
|
|
|
projectColor={projectColor}
|
|
|
|
|
|
priority={project.priority}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default CurrentWeek
|
|
|
|
|
|
|