Files
play-life/play-life-web/src/components/Tracking.jsx
2026-02-08 17:01:36 +03:00

194 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, 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, setWeeks] = useState(() => getLastFiveWeeks())
const [selectedWeek, setSelectedWeek] = useState(() => {
const initialWeeks = getLastFiveWeeks()
return initialWeeks[initialWeeks.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 updatedWeeks = getLastFiveWeeks()
setWeeks(updatedWeeks)
// Устанавливаем текущую неделю (последняя в списке)
const currentWeek = updatedWeeks[updatedWeeks.length - 1]
setSelectedWeek(currentWeek)
}
prevActiveTabRef.current = activeTab
}, [activeTab])
// Скролл к чипсу текущей недели при открытии экрана
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])
// Функция для обновления данных
const refreshData = 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)
}
}
// Загрузка данных при смене недели
useEffect(() => {
refreshData()
}, [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