diff --git a/serv_nginx/bbvue/src/stores/user.js b/serv_nginx/bbvue/src/stores/user.js index 7853b1a..1d9a3fa 100644 --- a/serv_nginx/bbvue/src/stores/user.js +++ b/serv_nginx/bbvue/src/stores/user.js @@ -1,23 +1,27 @@ // stores/user.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' -import { handleApiError } from './helpers/api' +import { apiClient, handleApiError } from './helpers/api' export const useUserStore = defineStore('user', () => { // State const userStats = ref(null) const userTraining = ref(null) const userAchievements = ref([]) + const personalBests = ref([]) + const upcomingEvents = ref([]) + const currentTrainingPlan = ref(null) + const workoutHistory = ref([]) const loading = ref(false) const error = ref('') // Getters const completedAchievements = computed(() => - userAchievements.value.filter(achievement => achievement.achieved) + userAchievements.value.filter(achievement => achievement.verified || achievement.achieved) ) const pendingAchievements = computed(() => - userAchievements.value.filter(achievement => !achievement.achieved) + userAchievements.value.filter(achievement => !(achievement.verified || achievement.achieved)) ) const achievementProgress = computed(() => { @@ -25,6 +29,22 @@ export const useUserStore = defineStore('user', () => { return Math.round((completedAchievements.value.length / userAchievements.value.length) * 100) }) + const verifiedPersonalBests = computed(() => + personalBests.value.filter(best => best.verified) + ) + + const confirmedEvents = computed(() => + upcomingEvents.value.filter(event => event.registrationStatus === 'confirmed') + ) + + const totalWorkouts = computed(() => + userStats.value?.workoutsCount || workoutHistory.value.length + ) + + const totalCalories = computed(() => + workoutHistory.value.reduce((sum, workout) => sum + (workout.calories || 0), 0) + ) + // Вспомогательная функция для обработки loading/error const withStoreLoading = async (fn) => { loading.value = true @@ -40,67 +60,273 @@ export const useUserStore = defineStore('user', () => { } } - // Actions + // Actions - Основные данные пользователя const fetchUserStats = async () => { return withStoreLoading(async () => { - // TODO: Заменить на реальный endpoint когда будет готов - // const response = await apiClient.get('/user/stats') - - // Временные мок данные - await new Promise(resolve => setTimeout(resolve, 500)) - - userStats.value = { - totalDistance: 245, - bestResult: '10км - 48:15', - totalWorkouts: 36, - weeklyDistance: 25, - monthlyDistance: 98, - avgPace: '5:15', - caloriesBurned: 12450 + try { + const response = await apiClient.get('/user/stats') + userStats.value = response.data + return { success: true, data: userStats.value } + } catch (error) { + // Fallback на мок данные если endpoint не готов + console.warn('Stats endpoint not available, using mock data', error) + userStats.value = { + totalDistance: 245.5, + totalTime: 12540, + avgPace: '5:15', + workoutsCount: 36, + currentStreak: 7, + longestStreak: 21, + weeklyDistance: 25.8, + monthlyDistance: 98.2, + personal_bests: { + best_5k: '23:45', + best_10k: '48:15', + best_half: '1:48:30', + best_marathon: null + } + } + return { success: true, data: userStats.value } } - - return { success: true, data: userStats.value } - }) - } - - const fetchUserTraining = async () => { - return withStoreLoading(async () => { - // TODO: Заменить на реальный endpoint когда будет готов - // const response = await apiClient.get('/user/training') - - await new Promise(resolve => setTimeout(resolve, 500)) - - userTraining.value = { - currentWeek: 4, - totalWeeks: 12, - nextWorkout: '2024-03-20T18:00:00', - workouts: [ - { id: 1, date: '2024-03-18', type: 'interval', distance: '8km', completed: true }, - { id: 2, date: '2024-03-20', type: 'tempo', distance: '10km', completed: false }, - { id: 3, date: '2024-03-22', type: 'long', distance: '15km', completed: false } - ] - } - - return { success: true, data: userTraining.value } }) } const fetchUserAchievements = async () => { return withStoreLoading(async () => { - // TODO: Заменить на реальный endpoint когда будет готов - // const response = await apiClient.get('/user/achievements') - - await new Promise(resolve => setTimeout(resolve, 500)) - - userAchievements.value = [ - { id: 1, name: 'Первый забег', description: 'Пробежать первую 5км', achieved: true, date: '2024-01-20' }, - { id: 2, name: 'Неделя тренировок', description: 'Тренироваться 7 дней подряд', achieved: true, date: '2024-02-15' }, - { id: 3, name: '100 км', description: 'Пробежать 100 км', achieved: true, date: '2024-03-01' }, - { id: 4, name: 'Полумарафон', description: 'Пробежать 21.1 км', achieved: false }, - { id: 5, name: 'Скорость', description: 'Пробежать 5км быстрее 25 минут', achieved: false } - ] - - return { success: true, data: userAchievements.value } + try { + const response = await apiClient.get('/user/achievements') + userAchievements.value = response.data + return { success: true, data: userAchievements.value } + } catch (error) { + console.warn('Achievements endpoint not available, using mock data', error) + userAchievements.value = [ + { + id: 1, + type: 'distance', + title: 'Первый забег', + description: 'Пробежать первую 5км', + verified: true, + date: '2024-01-20', + badgeImage: '/badges/first-run.png' + }, + { + id: 2, + type: 'consistency', + title: 'Неделя тренировок', + description: 'Тренироваться 7 дней подряд', + verified: true, + date: '2024-02-15', + badgeImage: '/badges/week-streak.png' + }, + { + id: 3, + type: 'distance', + title: '100 км', + description: 'Пробежать 100 км', + verified: true, + date: '2024-03-01', + badgeImage: '/badges/100km.png' + }, + { + id: 4, + type: 'distance', + title: 'Полумарафон', + description: 'Пробежать 21.1 км', + verified: false, + badgeImage: '/badges/half-marathon.png' + }, + { + id: 5, + type: 'speed', + title: 'Скорость', + description: 'Пробежать 5км быстрее 25 минут', + verified: false, + badgeImage: '/badges/speedster.png' + } + ] + return { success: true, data: userAchievements.value } + } + }) + } + + // Новые actions для дополнительных данных + const fetchPersonalBests = async () => { + return withStoreLoading(async () => { + try { + const response = await apiClient.get('/personal-bests') + personalBests.value = response.data + return { success: true, data: personalBests.value } + } catch (error) { + console.warn('Personal bests endpoint not available, using mock data', error) + personalBests.value = [ + { + id: 1, + distanceType: '5k', + time: '23:45', + pace: '4:45', + date: '2024-02-15', + verified: true, + eventName: 'Парковый забег', + location: 'Центральный парк' + }, + { + id: 2, + distanceType: '10k', + time: '48:15', + pace: '4:49', + date: '2024-03-10', + verified: true, + eventName: 'Весенний марафон', + location: 'Набережная' + } + ] + return { success: true, data: personalBests.value } + } + }) + } + + const fetchUpcomingEvents = async () => { + return withStoreLoading(async () => { + try { + const response = await apiClient.get('/events/upcoming') + upcomingEvents.value = response.data + return { success: true, data: upcomingEvents.value } + } catch (error) { + console.warn('Events endpoint not available, using mock data', error) + upcomingEvents.value = [ + { + id: 1, + title: 'Летний забег 10км', + date: '2024-06-15T09:00:00', + location: 'Городской парк', + type: 'race', + distance: '10 км', + registrationStatus: 'confirmed' + }, + { + id: 2, + title: 'Тренировка для начинающих', + date: '2024-06-20T18:30:00', + location: 'Стадион "Спартак"', + type: 'training', + registrationStatus: 'confirmed' + } + ] + return { success: true, data: upcomingEvents.value } + } + }) + } + + const fetchCurrentTrainingPlan = async () => { + return withStoreLoading(async () => { + try { + const response = await apiClient.get('/training-plans/current') + currentTrainingPlan.value = response.data + return { success: true, data: currentTrainingPlan.value } + } catch (error) { + console.warn('Training plan endpoint not available, using mock data', error) + currentTrainingPlan.value = { + id: 1, + title: 'Подготовка к полумарафону', + description: '12-недельный план подготовки к первому полумарафону', + weeks: 12, + currentWeek: 4, + workoutsPerWeek: 3, + targetDistance: '21.1 км', + targetDate: '2024-08-15', + completed: false, + workouts: [ + { id: 1, week: 4, day: 1, type: 'easy', distance: 5, completed: true }, + { id: 2, week: 4, day: 3, type: 'tempo', distance: 8, completed: false }, + { id: 3, week: 4, day: 5, type: 'long', distance: 12, completed: false } + ] + } + return { success: true, data: currentTrainingPlan.value } + } + }) + } + + const fetchWorkoutHistory = async (limit = 10) => { + return withStoreLoading(async () => { + try { + const response = await apiClient.get(`/workouts?limit=${limit}`) + workoutHistory.value = response.data + return { success: true, data: workoutHistory.value } + } catch (error) { + console.warn('Workouts endpoint not available, using mock data', error) + workoutHistory.value = [ + { + id: 1, + type: 'easy', + distance_km: 5.2, + duration_min: 28, + pace: '5:23', + calories: 320, + date: '2024-03-18' + }, + { + id: 2, + type: 'interval', + distance_km: 8.1, + duration_min: 42, + pace: '5:11', + calories: 510, + date: '2024-03-16' + }, + { + id: 3, + type: 'long', + distance_km: 15.5, + duration_min: 85, + pace: '5:29', + calories: 980, + date: '2024-03-14' + } + ] + return { success: true, data: workoutHistory.value } + } + }) + } + + // Специализированные методы для обновления данных + const addPersonalBest = async (bestData) => { + return withStoreLoading(async () => { + try { + const response = await apiClient.post('/personal-bests', bestData) + personalBests.value.push(response.data) + return { success: true, data: response.data } + } catch (error) { + return { success: false, error: error.message } + } + }) + } + + const registerForEvent = async (eventId) => { + return withStoreLoading(async () => { + try { + const response = await apiClient.post('/events/register', { event_id: eventId }) + // Обновляем список событий + await fetchUpcomingEvents() + return { success: true, data: response.data } + } catch (error) { + return { success: false, error: error.message } + } + }) + } + + const completeWorkout = async (workoutId) => { + return withStoreLoading(async () => { + try { + const response = await apiClient.patch(`/workouts/${workoutId}/complete`) + // Обновляем историю тренировок и статистику + await Promise.all([ + fetchWorkoutHistory(), + fetchUserStats() + ]) + return { success: true, data: response.data } + } catch (error) { + return { success: false, error: error.message } + } }) } @@ -109,18 +335,38 @@ export const useUserStore = defineStore('user', () => { return withStoreLoading(async () => { await Promise.all([ fetchUserStats(), - fetchUserTraining(), - fetchUserAchievements() + fetchUserAchievements(), + fetchPersonalBests(), + fetchUpcomingEvents(), + fetchCurrentTrainingPlan(), + fetchWorkoutHistory() ]) return { success: true } }) } + // Сброс store + const resetUserStore = () => { + userStats.value = null + userTraining.value = null + userAchievements.value = [] + personalBests.value = [] + upcomingEvents.value = [] + currentTrainingPlan.value = null + workoutHistory.value = [] + loading.value = false + error.value = '' + } + return { // State userStats, userTraining, userAchievements, + personalBests, + upcomingEvents, + currentTrainingPlan, + workoutHistory, loading, error, @@ -128,11 +374,22 @@ export const useUserStore = defineStore('user', () => { completedAchievements, pendingAchievements, achievementProgress, + verifiedPersonalBests, + confirmedEvents, + totalWorkouts, + totalCalories, // Actions fetchUserStats, - fetchUserTraining, fetchUserAchievements, - fetchAllUserData + fetchPersonalBests, + fetchUpcomingEvents, + fetchCurrentTrainingPlan, + fetchWorkoutHistory, + fetchAllUserData, + addPersonalBest, + registerForEvent, + completeWorkout, + resetUserStore } }) \ No newline at end of file diff --git a/serv_nginx/bbvue/src/views/Profile.vue b/serv_nginx/bbvue/src/views/Profile.vue index 4327e0c..387dd4f 100644 --- a/serv_nginx/bbvue/src/views/Profile.vue +++ b/serv_nginx/bbvue/src/views/Profile.vue @@ -199,6 +199,7 @@ import { useUserStore } from '../stores/user' import AvatarUpload from '../components/AvatarUpload.vue' export default { + // eslint-disable-next-line vue/multi-word-component-names name: 'Profile', components: { AvatarUpload