Files
play-life/play-life-web/src/components/CurrentWeek.jsx
poignatov 932dba8682
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 34s
Унификация отображения ошибок: LoadingError для загрузки, Toast для действий
2026-01-11 15:51:28 +03:00

190 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ProjectProgressBar from './ProjectProgressBar'
import LoadingError from './LoadingError'
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
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 (
<div className="fixed inset-0 bottom-20 flex justify-center items-center">
<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>
<div className="text-gray-600 font-medium">Загрузка...</div>
</div>
</div>
)
}
if (error && (!data || projectsData.length === 0)) {
return <LoadingError onRetry={onRetry} />
}
// Процент выполнения берем только из данных 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
})
// Получаем отсортированный список всех проектов для синхронизации цветов
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
}
// Сортируем: сначала по 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)
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
}) : []
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">
{hasProgressData && typeof overallProgress === 'number' && Number.isFinite(overallProgress)
? `${overallProgress.toFixed(1)}%`
: 'N/A'}
</div>
</div>
{hasProgressData && typeof overallProgress === 'number' && Number.isFinite(overallProgress) && (
<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"
strokeDasharray={`${Math.min(Math.max(overallProgress / 100, 0), 1) * 175.93} 175.93`}
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"
title="Проекты"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<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>
</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) => {
if (!project || !project.project_name) {
return null
}
const projectColor = getProjectColor(project.project_name, allProjects)
return (
<div key={index}>
<ProjectProgressBar
projectName={project.project_name}
totalScore={parseFloat(project.total_score) || 0}
minGoalScore={parseFloat(project.min_goal_score) || 0}
maxGoalScore={parseFloat(project.max_goal_score) || 0}
onProjectClick={onProjectClick}
projectColor={projectColor}
priority={project.priority}
/>
</div>
)
})}
</div>
</div>
)
}
export default CurrentWeek