Редизайн экрана проектов с круглыми прогрессами
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m7s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m7s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "3.18.1",
|
||||
"version": "3.19.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -2,6 +2,310 @@ 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 (
|
||||
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
|
||||
<svg className="transform -rotate-90" viewBox={`0 0 ${size} ${size}`}>
|
||||
{/* Фоновая окружность */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
className="text-gray-200"
|
||||
/>
|
||||
{/* Прогресс окружность */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke="url(#overall-gradient)"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1000 ease-out"
|
||||
/>
|
||||
{/* Галочка при 100% */}
|
||||
{showCheckmark && progress >= 100 && (
|
||||
<g className="transform rotate-90" style={{ transformOrigin: `${size / 2}px ${size / 2}px` }}>
|
||||
<path
|
||||
d={`M ${size / 2 - 12} ${size / 2} L ${size / 2 - 4} ${size / 2 + 8} L ${size / 2 + 12} ${size / 2 - 8}`}
|
||||
stroke="url(#overall-gradient)"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
<defs>
|
||||
<linearGradient id="overall-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#4f46e5" />
|
||||
<stop offset="100%" stopColor="#9333ea" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
{/* Процент в центре */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
{progress !== null && progress !== undefined ? `${progress.toFixed(0)}%` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Специальный компонент круглого прогрессбара с поддержкой экстра прогресса
|
||||
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 (
|
||||
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
|
||||
<svg className="transform -rotate-90" viewBox={`0 0 ${size} ${size}`}>
|
||||
{/* Фоновая окружность */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
className="text-gray-200"
|
||||
/>
|
||||
{/* Базовый прогресс окружность */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke="url(#project-gradient)"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1000 ease-out"
|
||||
/>
|
||||
{/* Экстра прогресс окружность (другой цвет) */}
|
||||
{normalizedExtraProgress > 0 && (
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke="url(#project-extra-gradient)"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={extraStrokeDasharray}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1000 ease-out"
|
||||
/>
|
||||
)}
|
||||
{/* Галочка при 100% */}
|
||||
{showCheckmark && progress >= 100 && (
|
||||
<g className="transform rotate-90" style={{ transformOrigin: `${size / 2}px ${size / 2}px` }}>
|
||||
<path
|
||||
d={`M ${size / 2 - 12} ${size / 2} L ${size / 2 - 4} ${size / 2 + 8} L ${size / 2 + 12} ${size / 2 - 8}`}
|
||||
stroke="url(#project-gradient)"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
<defs>
|
||||
<linearGradient id="project-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#4f46e5" />
|
||||
<stop offset="100%" stopColor="#9333ea" />
|
||||
</linearGradient>
|
||||
<linearGradient id="project-extra-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#f59e0b" />
|
||||
<stop offset="100%" stopColor="#d97706" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
{/* Процент в центре */}
|
||||
{percentage && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
{percentage}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент карточки проекта с круглым прогрессбаром
|
||||
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 (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className="bg-white rounded-2xl py-3 px-3 shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer border border-gray-200 hover:border-indigo-300"
|
||||
>
|
||||
{/* Верхняя часть с названием и прогрессом */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Левая часть - текст (название, баллы, целевая зона) */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base font-semibold text-gray-600 leading-normal truncate mb-1">
|
||||
{project_name}
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-black leading-normal mb-1">
|
||||
{total_score?.toFixed(1) || '0.0'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 leading-normal">
|
||||
Целевая зона: {getTargetZone()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая часть - круглый прогрессбар */}
|
||||
<div className="flex-shrink-0 ml-3">
|
||||
<ProjectCircularProgressBar
|
||||
progress={visualProgress}
|
||||
extraProgress={extraVisualProgress}
|
||||
size={80}
|
||||
strokeWidth={8}
|
||||
showCheckmark={false}
|
||||
percentage={goalProgress.toFixed(0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент группы проектов по приоритету
|
||||
function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick }) {
|
||||
if (projects.length === 0) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Заголовок группы */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h2 className="text-xl text-black">{title}</h2>
|
||||
<span className="text-black text-xl font-bold">•</span>
|
||||
<span className="text-lg font-bold text-black">{subtitle}</span>
|
||||
</div>
|
||||
|
||||
{/* Карточки проектов */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{projects.map((project, index) => {
|
||||
if (!project || !project.project_name) return null
|
||||
|
||||
const projectColor = getProjectColor(project.project_name, allProjects)
|
||||
|
||||
return (
|
||||
<ProjectCard
|
||||
key={index}
|
||||
project={project}
|
||||
projectColor={projectColor}
|
||||
onProjectClick={onProjectClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProjectsData, onNavigate }) {
|
||||
// Обрабатываем данные: может быть объект с projects и total, или просто массив
|
||||
const projectsData = data?.projects || (Array.isArray(data) ? data : []) || []
|
||||
@@ -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 (
|
||||
<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 className="relative pt-8">
|
||||
{/* Кнопка "Приоритеты" в правом верхнем углу */}
|
||||
{onNavigate && (
|
||||
<div className="absolute top-0 right-0 z-10">
|
||||
<button
|
||||
onClick={() => onNavigate('priorities')}
|
||||
className="flex items-center justify-center w-10 h-10 text-gray-600 hover:text-indigo-600 transition-colors duration-200"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Общий прогресс - большой круг в центре */}
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<div className="relative mb-6 cursor-pointer" onClick={() => onNavigate && onNavigate('full')}>
|
||||
<CircularProgressBar
|
||||
progress={displayOverallProgress}
|
||||
size={180}
|
||||
strokeWidth={12}
|
||||
/>
|
||||
{/* Подсказка при наведении */}
|
||||
<div className="absolute inset-0 rounded-full opacity-0 hover:opacity-100 transition-opacity duration-200 bg-black bg-opacity-10 flex items-center justify-center">
|
||||
<span className="text-xs text-gray-600 font-medium bg-white px-2 py-1 rounded shadow-sm">
|
||||
Открыть статистику
|
||||
</span>
|
||||
</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
|
||||
}
|
||||
{/* Группы проектов по приоритетам */}
|
||||
<div className="space-y-6">
|
||||
<PriorityGroup
|
||||
title="Главный"
|
||||
subtitle={`${mainProgress.toFixed(1)}%`}
|
||||
projects={priorityGroups.main}
|
||||
allProjects={allProjects}
|
||||
onProjectClick={onProjectClick}
|
||||
/>
|
||||
|
||||
const projectColor = getProjectColor(project.project_name, allProjects)
|
||||
<PriorityGroup
|
||||
title="Важные"
|
||||
subtitle={`${importantProgress.toFixed(1)}%`}
|
||||
projects={priorityGroups.important}
|
||||
allProjects={allProjects}
|
||||
onProjectClick={onProjectClick}
|
||||
/>
|
||||
|
||||
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>
|
||||
)
|
||||
})}
|
||||
<PriorityGroup
|
||||
title="Остальные"
|
||||
subtitle={`${othersProgress.toFixed(1)}%`}
|
||||
projects={priorityGroups.others}
|
||||
allProjects={allProjects}
|
||||
onProjectClick={onProjectClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user