159 lines
6.0 KiB
React
159 lines
6.0 KiB
React
|
|
function ProjectProgressBar({ projectName, totalScore, minGoalScore, maxGoalScore, onProjectClick, projectColor, priority }) {
|
||
|
|
// Вычисляем максимальное значение для шкалы (берем максимум из maxGoalScore и totalScore + 20%)
|
||
|
|
const maxScale = Math.max(maxGoalScore, totalScore * 1.2, 1)
|
||
|
|
|
||
|
|
// Процентные значения
|
||
|
|
const totalScorePercent = (totalScore / maxScale) * 100
|
||
|
|
const minGoalPercent = (minGoalScore / maxScale) * 100
|
||
|
|
const maxGoalPercent = (maxGoalScore / maxScale) * 100
|
||
|
|
const goalRangePercent = maxGoalPercent - minGoalPercent
|
||
|
|
|
||
|
|
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
|
||
|
|
})()
|
||
|
|
|
||
|
|
const goalProgress = (() => {
|
||
|
|
const safeTotal = Number.isFinite(totalScore) ? totalScore : 0
|
||
|
|
const safeMinGoal = Number.isFinite(minGoalScore) ? minGoalScore : 0
|
||
|
|
const safeMaxGoal = Number.isFinite(maxGoalScore) ? maxGoalScore : 0
|
||
|
|
|
||
|
|
// Если нет валидного 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 isGoalReached = totalScore >= minGoalScore
|
||
|
|
const isGoalExceeded = totalScore >= maxGoalScore
|
||
|
|
|
||
|
|
const priorityBorderStyle =
|
||
|
|
normalizedPriority === 1
|
||
|
|
? { borderColor: '#d4af37' }
|
||
|
|
: normalizedPriority === 2
|
||
|
|
? { borderColor: '#c0c0c0' }
|
||
|
|
: {}
|
||
|
|
|
||
|
|
const cardBorderClasses =
|
||
|
|
normalizedPriority === 1 || normalizedPriority === 2
|
||
|
|
? 'border-2'
|
||
|
|
: 'border border-gray-200/50 hover:border-indigo-300'
|
||
|
|
|
||
|
|
const cardBaseClasses =
|
||
|
|
'bg-gradient-to-br from-white to-gray-50 rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer'
|
||
|
|
|
||
|
|
const handleClick = () => {
|
||
|
|
if (onProjectClick) {
|
||
|
|
onProjectClick(projectName)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
onClick={handleClick}
|
||
|
|
className={`${cardBaseClasses} ${cardBorderClasses}`}
|
||
|
|
style={priorityBorderStyle}
|
||
|
|
>
|
||
|
|
<div className="flex justify-between items-center mb-2">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<div
|
||
|
|
className="w-2 h-2 rounded-full"
|
||
|
|
style={{ backgroundColor: projectColor }}
|
||
|
|
></div>
|
||
|
|
<h3 className="text-lg font-semibold text-gray-800">{projectName}</h3>
|
||
|
|
</div>
|
||
|
|
<div className="text-right">
|
||
|
|
<div className="text-lg font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||
|
|
{totalScore.toFixed(1)}
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-gray-500">
|
||
|
|
из {minGoalScore.toFixed(1)} ({goalProgress.toFixed(0)}%)
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="relative h-6 bg-gray-200 rounded-full overflow-hidden shadow-inner">
|
||
|
|
{/* Диапазон цели (min_goal_score до max_goal_score) */}
|
||
|
|
{minGoalScore > 0 && maxGoalScore > 0 && (
|
||
|
|
<div
|
||
|
|
className="absolute h-full bg-gradient-to-r from-amber-200 via-yellow-300 to-amber-200 opacity-70 border-l border-r border-amber-400"
|
||
|
|
style={{
|
||
|
|
left: `${minGoalPercent}%`,
|
||
|
|
width: `${goalRangePercent}%`,
|
||
|
|
}}
|
||
|
|
title={`Цель: ${minGoalScore.toFixed(2)} - ${maxGoalScore.toFixed(2)}`}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Текущее значение (total_score) */}
|
||
|
|
<div
|
||
|
|
className={`absolute h-full transition-all duration-700 ease-out ${
|
||
|
|
isGoalExceeded
|
||
|
|
? 'bg-gradient-to-r from-green-500 to-emerald-500'
|
||
|
|
: isGoalReached
|
||
|
|
? 'bg-gradient-to-r from-yellow-500 to-amber-500'
|
||
|
|
: 'bg-gradient-to-r from-indigo-500 to-purple-500'
|
||
|
|
} shadow-sm`}
|
||
|
|
style={{
|
||
|
|
width: `${totalScorePercent}%`,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div className="absolute inset-0 bg-white/20 animate-pulse"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Индикатор текущего значения */}
|
||
|
|
{totalScorePercent > 0 && (
|
||
|
|
<div
|
||
|
|
className="absolute top-0 h-full w-0.5 bg-white shadow-md"
|
||
|
|
style={{
|
||
|
|
left: `${totalScorePercent}%`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex justify-between items-center text-xs mt-1.5">
|
||
|
|
<span className="text-gray-400">0</span>
|
||
|
|
{minGoalScore > 0 && (
|
||
|
|
<div className="flex items-center gap-1.5">
|
||
|
|
<div className="w-1.5 h-1.5 rounded-full bg-amber-400"></div>
|
||
|
|
<span className="text-amber-700 font-medium text-xs">
|
||
|
|
Цель: {minGoalScore.toFixed(1)} - {maxGoalScore.toFixed(1)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
<span className="text-gray-400">{maxScale.toFixed(1)}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default ProjectProgressBar
|
||
|
|
|