All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 26s
242 lines
7.2 KiB
JavaScript
242 lines
7.2 KiB
JavaScript
import React from 'react'
|
|
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
} from 'chart.js'
|
|
import { Line } from 'react-chartjs-2'
|
|
import WeekProgressChart from './WeekProgressChart'
|
|
import { getAllProjectsSorted, getProjectColor, sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
|
|
|
|
// Экспортируем для обратной совместимости (если используется в других местах)
|
|
export { getProjectColorByIndex } from '../utils/projectUtils'
|
|
|
|
const parseWeekKey = (weekKey) => {
|
|
const [yearStr, weekStr] = weekKey.split('-W')
|
|
return { year: Number(yearStr), week: Number(weekStr) }
|
|
}
|
|
|
|
const compareWeekKeys = (a, b) => {
|
|
const [yearA, weekA] = a.split('-W').map(Number)
|
|
const [yearB, weekB] = b.split('-W').map(Number)
|
|
|
|
if (yearA !== yearB) {
|
|
return yearA - yearB
|
|
}
|
|
return weekA - weekB
|
|
}
|
|
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
)
|
|
|
|
function FullStatistics({ selectedProject, onClearSelection, data, loading, error, onRetry, currentWeekData, onNavigate }) {
|
|
|
|
const processData = () => {
|
|
if (!data || data.length === 0) return null
|
|
|
|
// Группируем данные по проектам
|
|
const projectsMap = {}
|
|
|
|
data.forEach(item => {
|
|
const projectName = item.project_name
|
|
const weekKey = `${item.report_year}-W${item.report_week.toString().padStart(2, '0')}`
|
|
|
|
if (!projectsMap[projectName]) {
|
|
projectsMap[projectName] = {}
|
|
}
|
|
|
|
projectsMap[projectName][weekKey] = parseFloat(item.total_score)
|
|
})
|
|
|
|
// Собираем все уникальные недели и сортируем их по году и неделе
|
|
const allWeeks = new Set()
|
|
Object.values(projectsMap).forEach(weeks => {
|
|
Object.keys(weeks).forEach(week => allWeeks.add(week))
|
|
})
|
|
const sortedWeeks = Array.from(allWeeks).sort(compareWeekKeys)
|
|
|
|
// Получаем отсортированный список всех проектов для синхронизации цветов
|
|
const allProjectNames = getAllProjectsSorted(data)
|
|
|
|
// Фильтруем по выбранному проекту, если он указан
|
|
let projectNames = allProjectNames.filter(name => projectsMap[name])
|
|
|
|
// Сортируем проекты так же, как на экране списка проектов (по priority и min_goal_score)
|
|
if (currentWeekData) {
|
|
projectNames = sortProjectsLikeCurrentWeek(projectNames, currentWeekData)
|
|
}
|
|
|
|
if (selectedProject) {
|
|
projectNames = projectNames.filter(name => name === selectedProject)
|
|
}
|
|
|
|
const datasets = projectNames.map((projectName) => {
|
|
let cumulativeSum = 0
|
|
const values = sortedWeeks.map(week => {
|
|
const score = projectsMap[projectName]?.[week] || 0
|
|
cumulativeSum += score
|
|
return cumulativeSum
|
|
})
|
|
|
|
// Генерируем цвет для линии на основе индекса в полном списке проектов
|
|
const color = getProjectColor(projectName, allProjectNames)
|
|
|
|
return {
|
|
label: projectName,
|
|
data: values,
|
|
borderColor: color,
|
|
backgroundColor: color.replace('50%)', '20%)'),
|
|
fill: false,
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
}
|
|
})
|
|
|
|
return {
|
|
labels: sortedWeeks,
|
|
datasets: datasets,
|
|
}
|
|
}
|
|
|
|
const chartData = processData()
|
|
|
|
// Показываем loading только если данных нет и идет загрузка
|
|
if (loading && !chartData) {
|
|
return (
|
|
<div className="flex justify-center items-center py-16">
|
|
<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 && !chartData) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-16">
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-4 max-w-md">
|
|
<div className="text-red-700 font-semibold mb-2">Ошибка загрузки</div>
|
|
<div className="text-red-600 text-sm">{error}</div>
|
|
</div>
|
|
<button
|
|
onClick={onRetry}
|
|
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl font-semibold"
|
|
>
|
|
Попробовать снова
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 15,
|
|
font: {
|
|
size: 12,
|
|
},
|
|
},
|
|
padding: {
|
|
top: 20,
|
|
},
|
|
},
|
|
title: {
|
|
display: false,
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
callbacks: {
|
|
label: function(context) {
|
|
return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}`
|
|
}
|
|
}
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
title: {
|
|
display: false,
|
|
},
|
|
grid: {
|
|
display: true,
|
|
color: 'rgba(0, 0, 0, 0.05)',
|
|
},
|
|
},
|
|
y: {
|
|
display: true,
|
|
title: {
|
|
display: false,
|
|
},
|
|
beginAtZero: true,
|
|
grid: {
|
|
display: true,
|
|
color: 'rgba(0, 0, 0, 0.05)',
|
|
},
|
|
},
|
|
},
|
|
interaction: {
|
|
mode: 'nearest',
|
|
axis: 'x',
|
|
intersect: false,
|
|
},
|
|
}
|
|
|
|
if (!chartData) {
|
|
return (
|
|
<div className="flex justify-center items-center py-16">
|
|
<div className="text-gray-500 text-lg">Нет данных для отображения</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{onNavigate && (
|
|
<div className="flex justify-end mb-4">
|
|
<button
|
|
onClick={() => onNavigate('current')}
|
|
className="flex items-center justify-center w-10 h-10 rounded-full bg-white hover:bg-gray-100 text-gray-600 hover:text-gray-800 border border-gray-200 hover:border-gray-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="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
<div style={{ height: '550px' }}>
|
|
<Line data={chartData} options={chartOptions} />
|
|
</div>
|
|
<WeekProgressChart data={data} allProjectsSorted={getAllProjectsSorted(data)} currentWeekData={currentWeekData} selectedProject={selectedProject} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default FullStatistics
|
|
|