460 lines
16 KiB
TypeScript
460 lines
16 KiB
TypeScript
"use client"
|
||
|
||
import { useEffect, useState } from "react"
|
||
import { useHistory } from "react-router-dom"
|
||
// import { useParams } from "react-router-dom"
|
||
|
||
import { CalendarIcon } from "../components/icons/CalendarIcon"
|
||
import { DumbbellIcon } from "../components/icons/DumbbellIcon"
|
||
|
||
import HeaderNav from "../components/HeaderNav"
|
||
import BottomNavigation from "../components/BottomNavigation"
|
||
import CircularProgressDisplay from "../components/CircularProgressDisplay"
|
||
|
||
import { StatCardHome } from "../components/cards/StatCardHome"
|
||
import { WorkoutCardHome } from "../components/cards/WorkoutCardHome"
|
||
|
||
import { connect } from "../confconnect"
|
||
import { getRouteExerciseByIndex } from "../shared/consts/router"
|
||
import { getRouteCourses } from "../shared/consts/router"
|
||
import { getRouteCourseExercises } from "../shared/consts/router"
|
||
|
||
import type { Course, User, CoursesApiResponse } from "../types/course"
|
||
|
||
|
||
interface CourseExercises {
|
||
id_exercise: number
|
||
day: number
|
||
position: number
|
||
title: string
|
||
desc: string
|
||
time: string
|
||
count: number
|
||
repeats: number
|
||
}
|
||
|
||
interface CurrentExercise {
|
||
courseId: string
|
||
exerciseId: string
|
||
exerciseIndex: number
|
||
day: number
|
||
title: string
|
||
position: number
|
||
isCompleted: boolean
|
||
}
|
||
|
||
interface ProgressStats {
|
||
completedExercises: number
|
||
totalExercises: number
|
||
completedCourses: number
|
||
totalCourses: number
|
||
overallProgress: number
|
||
}
|
||
|
||
export default function Home() {
|
||
const history = useHistory()
|
||
const [currentDate, setCurrentDate] = useState("")
|
||
const [error, setError] = useState<string>("")
|
||
const [courses, setCourses] = useState<Course[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
const [currentExercise, setCurrentExercise] = useState<CurrentExercise | null>(null)
|
||
const [loadingCurrentExercise, setLoadingCurrentExercise] = useState(false)
|
||
|
||
const [progressStats, setProgressStats] = useState<ProgressStats>({
|
||
completedExercises: 0,
|
||
totalExercises: 0,
|
||
completedCourses: 0,
|
||
totalCourses: 0,
|
||
overallProgress: 0,
|
||
})
|
||
|
||
const token = localStorage.getItem("authToken")
|
||
|
||
const calculateProgressStats = async (coursesList: Course[]): Promise<ProgressStats> => {
|
||
let totalExercises = 0
|
||
let completedExercises = 0
|
||
let completedCourses = 0
|
||
|
||
for (const course of coursesList) {
|
||
try {
|
||
// Получаем упражнения для каждого курса
|
||
const response = await connect.get(`/pacient/${course.id}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
})
|
||
console.log("!получили по запросу", response.data)
|
||
|
||
//упражнения курса
|
||
const exercises = response.data.course_exercises || []
|
||
|
||
totalExercises += exercises.length
|
||
|
||
let courseCompletedExercises = 0
|
||
|
||
// Проверяем каждое упражнение на завершенность
|
||
for (const exercise of exercises) {
|
||
const storageKey = `exerciseProgress_${course.id}_${exercise.id_exercise}_day_${exercise.day}`
|
||
const savedProgress = localStorage.getItem(storageKey)
|
||
|
||
if (savedProgress) {
|
||
const progress = JSON.parse(savedProgress)
|
||
const isCompleted =
|
||
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
|
||
|
||
if (isCompleted) {
|
||
completedExercises++
|
||
courseCompletedExercises++
|
||
}
|
||
}
|
||
}
|
||
|
||
// Курс считается завершенным если все упражнения выполнены
|
||
if (exercises.length > 0 && courseCompletedExercises === exercises.length) {
|
||
completedCourses++
|
||
}
|
||
} catch (error) {
|
||
console.error(`Ошибка при получении упражнений для курса ${course.id}:`, error)
|
||
// Если не удалось загрузить упражнения, считаем что в курсе 0 упражнений
|
||
}
|
||
}
|
||
|
||
const overallProgress = totalExercises > 0 ? Math.round((completedExercises / totalExercises) * 100) : 0
|
||
|
||
return {
|
||
completedExercises,
|
||
totalExercises,
|
||
completedCourses,
|
||
totalCourses: coursesList.length,
|
||
overallProgress,
|
||
}
|
||
}
|
||
|
||
const getCurrentExercise = async () => {
|
||
if (courses.length === 0) return
|
||
|
||
setLoadingCurrentExercise(true)
|
||
|
||
try {
|
||
let targetCourse: Course | null = null
|
||
|
||
// Проходим по всем курсам и ищем незавершенный
|
||
for (const course of courses) {
|
||
// Загружаем упражнения для каждого курса
|
||
const response = await connect.get(`/pacient/${course.id}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
})
|
||
|
||
const exercises = response.data.course_exercises || []
|
||
|
||
if (exercises.length === 0) continue // Пропускаем курсы без упражнений
|
||
|
||
// Проверяем, есть ли незавершенные упражнения в этом курсе
|
||
let hasIncompleteExercises = false
|
||
|
||
for (const exercise of exercises) {
|
||
const storageKey = `exerciseProgress_${course.id}_${exercise.id_exercise}_day_${exercise.day}`
|
||
const savedProgress = localStorage.getItem(storageKey)
|
||
|
||
let isCompleted = false
|
||
if (savedProgress) {
|
||
const progress = JSON.parse(savedProgress)
|
||
isCompleted =
|
||
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
|
||
}
|
||
|
||
if (!isCompleted) {
|
||
hasIncompleteExercises = true
|
||
break // Нашли незавершенное упражнение, можно остановиться
|
||
}
|
||
}
|
||
|
||
// Если в курсе есть незавершенные упражнения, выбираем его
|
||
if (hasIncompleteExercises) {
|
||
targetCourse = course
|
||
break // Берем первый найденный незавершенный курс
|
||
}
|
||
}
|
||
|
||
// Если все курсы завершены, берем последний курс
|
||
if (!targetCourse && courses.length > 0) {
|
||
targetCourse = courses[courses.length - 1]
|
||
}
|
||
|
||
if (!targetCourse) {
|
||
setCurrentExercise(null)
|
||
return
|
||
}
|
||
|
||
// Загружаем упражнения выбранного курса
|
||
const response = await connect.get(`/pacient/${targetCourse.id}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
})
|
||
|
||
const exercises = response.data.course_exercises || []
|
||
|
||
if (exercises.length === 0) {
|
||
setCurrentExercise(null)
|
||
return
|
||
}
|
||
|
||
// Группируем упражнения по дням
|
||
const exercisesByDay = exercises.reduce((acc: any, exercise: CourseExercises) => {
|
||
if (!acc[exercise.day]) {
|
||
acc[exercise.day] = []
|
||
}
|
||
acc[exercise.day].push(exercise)
|
||
return acc
|
||
}, {})
|
||
|
||
// Ищем первое незавершенное упражнение
|
||
let foundExercise: CurrentExercise | null = null
|
||
|
||
for (const day of Object.keys(exercisesByDay).sort((a, b) => Number(a) - Number(b))) {
|
||
const dayExercises = exercisesByDay[day].sort(
|
||
(a: CourseExercises, b: CourseExercises) => a.position - b.position,
|
||
)
|
||
|
||
for (let i = 0; i < dayExercises.length; i++) {
|
||
const exercise = dayExercises[i]
|
||
|
||
// Проверяем прогресс упражнения в localStorage
|
||
const storageKey = `exerciseProgress_${targetCourse.id}_${exercise.id_exercise}_day_${day}`
|
||
const savedProgress = localStorage.getItem(storageKey)
|
||
|
||
let isCompleted = false
|
||
if (savedProgress) {
|
||
const progress = JSON.parse(savedProgress)
|
||
isCompleted =
|
||
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
|
||
}
|
||
|
||
if (!isCompleted) {
|
||
foundExercise = {
|
||
courseId: targetCourse.id.toString(),
|
||
exerciseId: exercise.id_exercise.toString(),
|
||
exerciseIndex: i,
|
||
day: Number(day),
|
||
title: exercise.title,
|
||
position: exercise.position,
|
||
isCompleted: false,
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
if (foundExercise) break
|
||
}
|
||
|
||
// Если все упражнения завершены, берем последнее
|
||
if (!foundExercise && exercises.length > 0) {
|
||
const lastExercise = exercises[exercises.length - 1]
|
||
foundExercise = {
|
||
courseId: targetCourse.id.toString(),
|
||
exerciseId: lastExercise.id_exercise.toString(),
|
||
exerciseIndex: exercises.length - 1,
|
||
day: lastExercise.day,
|
||
title: lastExercise.title,
|
||
position: lastExercise.position,
|
||
isCompleted: true,
|
||
}
|
||
}
|
||
|
||
setCurrentExercise(foundExercise)
|
||
} catch (error) {
|
||
console.error("Ошибка при получении текущего упражнения:", error)
|
||
} finally {
|
||
setLoadingCurrentExercise(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
console.log(token)
|
||
if (!token) {
|
||
setError("Токен не найден")
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
setCurrentDate(
|
||
new Date().toLocaleDateString("ru-RU", {
|
||
year: "numeric",
|
||
month: "long",
|
||
day: "numeric",
|
||
}),
|
||
)
|
||
|
||
setLoading(true)
|
||
connect
|
||
.get<CoursesApiResponse>("/pacient/courses")
|
||
.then((response) => {
|
||
console.log("Response data:", response.data)
|
||
|
||
const users = response.data.courses || []
|
||
const allCourses: Course[] = []
|
||
|
||
users.forEach((user: User) => {
|
||
if (user.Courses && Array.isArray(user.Courses)) {
|
||
user.Courses.forEach((course) => {
|
||
allCourses.push({
|
||
id: course.id,
|
||
title: course.title,
|
||
desc: course.desc,
|
||
url_file_img: course.url_file_img,
|
||
course_exercises: course.course_exercises,
|
||
})
|
||
})
|
||
}
|
||
})
|
||
|
||
setCourses(allCourses)
|
||
setError("")
|
||
})
|
||
.catch((error) => {
|
||
if (error.response) {
|
||
console.error("Ошибка ответа сервера:", error.response.status, error.response.data)
|
||
setError(`Ошибка сервера: ${error.response.status}`)
|
||
} else if (error.request) {
|
||
console.error("Нет ответа от сервера:", error.request)
|
||
setError("Нет ответа от сервера")
|
||
} else {
|
||
console.error("Ошибка при настройке запроса:", error.message)
|
||
setError(`Ошибка: ${error.message}`)
|
||
}
|
||
})
|
||
.finally(() => {
|
||
setLoading(false)
|
||
})
|
||
}, [token])
|
||
|
||
useEffect(() => {
|
||
if (courses.length > 0 && !loading) {
|
||
getCurrentExercise()
|
||
|
||
calculateProgressStats(courses).then((stats) => {
|
||
setProgressStats(stats)
|
||
})
|
||
}
|
||
}, [courses, loading])
|
||
|
||
const handleWorkoutClick = () => {
|
||
if (currentExercise) {
|
||
// Используем правильный роут с day параметром
|
||
history.push(
|
||
getRouteExerciseByIndex(currentExercise.courseId, currentExercise.exerciseIndex, currentExercise.day),
|
||
)
|
||
} else {
|
||
// Fallback если нет текущего упражнения
|
||
history.push(getRouteExerciseByIndex(1, 0, 1))
|
||
}
|
||
}
|
||
|
||
const handleBackClick = () => {
|
||
history.goBack()
|
||
}
|
||
|
||
const handleCoursesClick = () => {
|
||
history.push(getRouteCourses())
|
||
}
|
||
|
||
const handleExercisesClick = () => {
|
||
if (currentExercise) {
|
||
// Используем courseId из текущего упражнения (незавершенный курс)
|
||
history.push(getRouteCourseExercises(Number.parseInt(currentExercise.courseId)))
|
||
} else if (courses.length > 0) {
|
||
// Fallback к первому курсу если нет текущего упражнения
|
||
history.push(getRouteCourseExercises(courses[0].id))
|
||
} else {
|
||
// Fallback к странице курсов если нет курсов
|
||
history.push(getRouteCourses())
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||
<div className="my-36 min-h-screen max-w-4xl mx-auto">
|
||
<HeaderNav item="Прогресс" text="Загрузка..." />
|
||
<div className="flex justify-center items-center py-20">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#2BACBE]"></div>
|
||
<span className="ml-3 text-gray-600">Загрузка данных...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||
<div className="my-36 min-h-screen max-w-4xl mx-auto">
|
||
<HeaderNav item="Прогресс" text={currentDate} />
|
||
|
||
<div className="bg-white rounded-3xl p-6 shadow-lg mx-4 sm:mx-6">
|
||
<div className="flex content-center items-center justify-between"></div>
|
||
<div className="flex justify-between items-center">
|
||
<div className="flex flex-col gap-6">
|
||
<div className="text-left">
|
||
<div className="text-sm sm:text-base text-gray-800">Все курсы</div>
|
||
<div className="text-2xl font-bold text-orange-500">
|
||
{progressStats.completedCourses}/{progressStats.totalCourses}
|
||
</div>
|
||
</div>
|
||
<div className="text-left">
|
||
<div className="text-sm sm:text-base text-gray-800">Все упражнения</div>
|
||
<div className="text-2xl font-bold text-cyan-400">
|
||
{progressStats.completedExercises}/{progressStats.totalExercises}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-center items-center gap-8">
|
||
<CircularProgressDisplay
|
||
overallProgress={progressStats.overallProgress}
|
||
totalCourses={progressStats.totalCourses}
|
||
totalExercises={progressStats.totalExercises}
|
||
completedCourses={progressStats.completedCourses}
|
||
completedExercises={progressStats.completedExercises}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-4 sm:px-6 space-y-6">
|
||
<WorkoutCardHome
|
||
onBackClick={handleBackClick}
|
||
onCardClick={handleWorkoutClick}
|
||
currentExercise={currentExercise}
|
||
loading={loadingCurrentExercise}
|
||
/>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-5">
|
||
<StatCardHome
|
||
title="Курсы"
|
||
subtitle="назначенные"
|
||
icon={CalendarIcon}
|
||
fill="#2BACBE"
|
||
onClick={handleCoursesClick}
|
||
/>
|
||
<StatCardHome
|
||
title="Упражнения"
|
||
subtitle="текущего курса"
|
||
icon={DumbbellIcon}
|
||
fill="#FF8D28"
|
||
onClick={handleExercisesClick}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<BottomNavigation />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|