Files
play-life/play-life-web/src/components/ProjectProgressBar.jsx
2025-12-29 20:01:55 +03:00

159 lines
6.0 KiB
JavaScript

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