All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m31s
185 lines
6.8 KiB
JavaScript
185 lines
6.8 KiB
JavaScript
import React, { useState, useEffect, useMemo, useRef } from 'react'
|
||
import { useAuth } from './auth/AuthContext'
|
||
import './Tracking.css'
|
||
|
||
// Функция для вычисления номера недели ISO
|
||
function getISOWeek(date) {
|
||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
||
const dayNum = d.getUTCDay() || 7
|
||
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
|
||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
||
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
|
||
}
|
||
|
||
// Функция для вычисления 5 недель (текущая + 4 предыдущие)
|
||
function getLastFiveWeeks() {
|
||
const weeks = []
|
||
const now = new Date()
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
const date = new Date(now)
|
||
date.setDate(date.getDate() - i * 7)
|
||
const week = getISOWeek(date)
|
||
const year = date.getFullYear()
|
||
weeks.push({
|
||
year,
|
||
week,
|
||
isCurrent: i === 0
|
||
})
|
||
}
|
||
return weeks.reverse() // От старой к новой
|
||
}
|
||
|
||
function Tracking({ onNavigate, activeTab }) {
|
||
const { authFetch } = useAuth()
|
||
const weeks = useMemo(() => getLastFiveWeeks(), [])
|
||
const [selectedWeek, setSelectedWeek] = useState(weeks[weeks.length - 1]) // Текущая неделя
|
||
const [data, setData] = useState(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState(null)
|
||
const scrollContainerRef = useRef(null)
|
||
const currentWeekChipRef = useRef(null)
|
||
const prevActiveTabRef = useRef(null)
|
||
|
||
// Сброс выбранной недели на текущую при открытии экрана
|
||
useEffect(() => {
|
||
// Проверяем, что экран только что открылся (activeTab стал 'tracking')
|
||
if (activeTab === 'tracking' && prevActiveTabRef.current !== 'tracking') {
|
||
const currentWeek = weeks[weeks.length - 1] // Последняя неделя в списке - текущая
|
||
setSelectedWeek(currentWeek)
|
||
}
|
||
prevActiveTabRef.current = activeTab
|
||
}, [activeTab, weeks])
|
||
|
||
// Скролл к чипсу текущей недели при открытии экрана
|
||
useEffect(() => {
|
||
// Выполняем скролл только когда экран открыт и только что открылся
|
||
if (activeTab === 'tracking' && currentWeekChipRef.current && scrollContainerRef.current) {
|
||
const chip = currentWeekChipRef.current
|
||
const container = scrollContainerRef.current
|
||
|
||
// Небольшая задержка для гарантии рендеринга
|
||
setTimeout(() => {
|
||
const chipLeft = chip.offsetLeft
|
||
const chipWidth = chip.offsetWidth
|
||
const containerWidth = container.offsetWidth
|
||
const scrollLeft = chipLeft - (containerWidth / 2) + (chipWidth / 2)
|
||
|
||
container.scrollTo({
|
||
left: scrollLeft,
|
||
behavior: 'smooth'
|
||
})
|
||
}, 100)
|
||
}
|
||
}, [activeTab])
|
||
|
||
// Загрузка данных при смене недели
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
setLoading(true)
|
||
setError(null)
|
||
try {
|
||
const res = await authFetch(`/api/tracking/stats?year=${selectedWeek.year}&week=${selectedWeek.week}`)
|
||
if (res.ok) {
|
||
setData(await res.json())
|
||
} else {
|
||
setError('Ошибка загрузки')
|
||
}
|
||
} catch (err) {
|
||
setError('Ошибка загрузки')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
fetchData()
|
||
}, [selectedWeek, authFetch])
|
||
|
||
return (
|
||
<div className="tracking-screen max-w-2xl mx-auto">
|
||
{/* Заголовок с крестиком */}
|
||
<div className="tracking-header">
|
||
<h2 className="text-2xl font-semibold text-gray-800">Отслеживание</h2>
|
||
</div>
|
||
<button className="close-x-button" onClick={() => window.history.back()}>✕</button>
|
||
|
||
{/* Чипсы недель с кнопкой доступов */}
|
||
<div className="week-controls">
|
||
<div className="week-chips-scroll" ref={scrollContainerRef}>
|
||
{weeks.map(w => (
|
||
<button
|
||
key={`${w.year}-${w.week}`}
|
||
ref={w.isCurrent ? currentWeekChipRef : null}
|
||
onClick={() => setSelectedWeek(w)}
|
||
className={`week-chip
|
||
${selectedWeek.year === w.year && selectedWeek.week === w.week ? 'selected' : ''}
|
||
${w.isCurrent ? 'current' : ''}`}
|
||
>
|
||
Неделя {w.week}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button
|
||
className="access-icon-btn"
|
||
onClick={() => onNavigate('tracking-access')}
|
||
title="Управление доступами"
|
||
>
|
||
<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.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Контент */}
|
||
{loading ? (
|
||
<div className="loading-spinner">Загрузка...</div>
|
||
) : error ? (
|
||
<div className="error-message">{error}</div>
|
||
) : (
|
||
<div className="users-list">
|
||
{data?.users.map(user => (
|
||
<UserTrackingCard key={user.user_id} user={user} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Карточка пользователя с прогрессом
|
||
function UserTrackingCard({ user }) {
|
||
// Сортируем проекты по priority (1, 2, остальные)
|
||
const sortedProjects = [...user.projects].sort((a, b) => {
|
||
const pa = a.priority ?? 99
|
||
const pb = b.priority ?? 99
|
||
return pa - pb
|
||
})
|
||
|
||
const totalPercent = user.total || 0
|
||
const getPercentColorClass = (percent) => {
|
||
return percent >= 100 ? 'percent-green' : 'percent-blue'
|
||
}
|
||
|
||
return (
|
||
<div className={`user-tracking-card ${user.is_current_user ? 'current-user' : ''}`}>
|
||
<div className="user-header">
|
||
<span className="user-name">{user.user_name}</span>
|
||
<span className={`user-total ${getPercentColorClass(totalPercent)}`}>{totalPercent.toFixed(0)}%</span>
|
||
</div>
|
||
<div className="projects-list">
|
||
{sortedProjects.map((project, idx) => {
|
||
const projectPercent = project.calculated_score || 0
|
||
return (
|
||
<div key={idx} className="project-row">
|
||
<span className="project-name">{project.project_name}</span>
|
||
<span className={`project-score ${getPercentColorClass(projectPercent)}`}>{projectPercent.toFixed(0)}%</span>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Tracking
|