Initial commit

This commit is contained in:
poignatov
2025-12-29 20:01:55 +03:00
commit 4f8a793377
63 changed files with 13655 additions and 0 deletions

View File

@@ -0,0 +1,289 @@
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 formatWeekKey = ({ year, week }) => `${year}-W${week.toString().padStart(2, '0')}`
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
}
// Возвращает понедельник ISO-недели
const getDateOfISOWeek = (week, year) => {
const simple = new Date(year, 0, 1 + (week - 1) * 7)
const dayOfWeek = simple.getDay() || 7 // Sunday -> 7
if (dayOfWeek !== 1) {
simple.setDate(simple.getDate() + (1 - dayOfWeek))
}
simple.setHours(0, 0, 0, 0)
return simple
}
const getISOWeekInfo = (date) => {
const target = new Date(date.getTime())
target.setHours(0, 0, 0, 0)
target.setDate(target.getDate() + 4 - (target.getDay() || 7))
const year = target.getFullYear()
const yearStart = new Date(year, 0, 1)
const week = Math.ceil(((target - yearStart) / 86400000 + 1) / 7)
return { year, week }
}
const getPrevWeekKey = (weekKey) => {
const { year, week } = parseWeekKey(weekKey)
const currentWeekDate = getDateOfISOWeek(week, year)
const prevWeekDate = new Date(currentWeekDate.getTime())
prevWeekDate.setDate(prevWeekDate.getDate() - 7)
const prevWeekInfo = getISOWeekInfo(prevWeekDate)
return formatWeekKey(prevWeekInfo)
}
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)
})
// Добавляем дополнительную неделю со значением 0,
// если первая неделя проекта имеет ненулевое значение
Object.values(projectsMap).forEach((weeks) => {
const projectWeeks = Object.keys(weeks)
if (!projectWeeks.length) return
const sortedProjectWeeks = projectWeeks.sort(compareWeekKeys)
const firstWeekKey = sortedProjectWeeks[0]
const firstScore = weeks[firstWeekKey]
if (firstScore !== 0) {
const zeroWeekKey = getPrevWeekKey(firstWeekKey)
weeks[zeroWeekKey] = 0
}
})
// Собираем все уникальные недели и сортируем их по году и неделе
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