diff --git a/main_dc/BB/bbvue/src/App.vue b/main_dc/BB/bbvue/src/App.vue
index 662eeed..cac64c6 100644
--- a/main_dc/BB/bbvue/src/App.vue
+++ b/main_dc/BB/bbvue/src/App.vue
@@ -41,8 +41,8 @@
+
+
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/components/RegistrationCard.vue b/main_dc/BB/bbvue/src/components/RegistrationCard.vue
new file mode 100644
index 0000000..effe8a9
--- /dev/null
+++ b/main_dc/BB/bbvue/src/components/RegistrationCard.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+ Дата:
+ {{ formatDate(registration.event.date) }}
+
+
+ Место:
+ {{ registration.event.location }}
+
+
+ Заметки:
+ {{ registration.notes }}
+
+
+ Результат:
+ {{ registration.result_time }}
+
+
+
+
+
+
+
+
+ ⏳ Результат ожидается
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/router/index.js b/main_dc/BB/bbvue/src/router/index.js
index b87eafa..eb37559 100644
--- a/main_dc/BB/bbvue/src/router/index.js
+++ b/main_dc/BB/bbvue/src/router/index.js
@@ -5,6 +5,18 @@ import { useAuthStore } from '../stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
+ {
+ path: '/stats/detailed',
+ name: 'DetailedStats',
+ component: () => import('../views/DetailedStats.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/stats/personal-bests',
+ name: 'PersonalBests',
+ component: () => import('../views/PersonalBests.vue'),
+ meta: { requiresAuth: true }
+ },
{
path: '/verify-email',
name: 'VerifyEmail',
@@ -19,7 +31,7 @@ const router = createRouter({
},
{
path: '/forgot-password',
- name: 'ForgotPassword',
+ name: 'ForgotPassword',
component: () => import('../views/ForgotPassword.vue'),
meta: { guestOnly: true }
},
@@ -102,6 +114,17 @@ const router = createRouter({
path: '/logout',
name: 'Logout',
component: () => import('../views/Logout.vue')
+ },
+ {
+ path: '/events',
+ name: 'Events',
+ component: () => import('../views/Events.vue')
+ },
+ // Можно добавить маршрут для детальной страницы события
+ {
+ path: '/events/:id',
+ name: 'EventDetails',
+ component: () => import('../views/DetailedStats.vue') // Создайте при необходимости
}
]
})
diff --git a/main_dc/BB/bbvue/src/stores/event.js b/main_dc/BB/bbvue/src/stores/event.js
new file mode 100644
index 0000000..fc7928f
--- /dev/null
+++ b/main_dc/BB/bbvue/src/stores/event.js
@@ -0,0 +1,313 @@
+// stores/events.js
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { apiClient, withLoading } from './helpers/api'
+
+export const useEventsStore = defineStore('events', () => {
+ // State
+ const events = ref([])
+ const userRegistrations = ref([])
+ const eventDetails = ref(null)
+ const loading = ref(false)
+ const error = ref('')
+
+ // Getters
+ const upcomingEvents = computed(() =>
+ events.value
+ .filter(event => new Date(event.date) > new Date())
+ .sort((a, b) => new Date(a.date) - new Date(b.date))
+ )
+
+ const pastEvents = computed(() =>
+ events.value
+ .filter(event => new Date(event.date) <= new Date())
+ .sort((a, b) => new Date(b.date) - new Date(a.date))
+ )
+
+ const registeredEvents = computed(() =>
+ userRegistrations.value
+ .filter(reg => reg.status === 'confirmed')
+ .map(reg => ({
+ ...reg.event,
+ registration_status: reg.status,
+ registration_id: reg.id,
+ result_time: reg.result_time
+ }))
+ )
+
+ const pendingRegistrations = computed(() =>
+ userRegistrations.value.filter(reg => reg.status === 'pending')
+ )
+
+ const eventTypes = {
+ 'race': 'Забег',
+ 'training': 'Тренировка',
+ 'social': 'Встреча',
+ 'workshop': 'Семинар'
+ }
+
+ // Actions
+ const fetchEvents = async (params = {}) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const queryString = new URLSearchParams(params).toString()
+ const response = await apiClient.get(`/events?${queryString}`)
+ events.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Events endpoint not available, using mock data', error)
+ events.value = generateMockEvents()
+ return { success: true, data: events.value }
+ }
+ })
+ }
+
+ const fetchUpcomingEvents = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/events/upcoming')
+ events.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Upcoming events endpoint not available', error)
+ return { success: true, data: upcomingEvents.value }
+ }
+ })
+ }
+
+ const fetchEventDetails = async (eventId) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get(`/events/${eventId}`)
+ eventDetails.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Event details endpoint not available', error)
+ eventDetails.value = events.value.find(event => event.id === parseInt(eventId)) || null
+ return { success: true, data: eventDetails.value }
+ }
+ })
+ }
+
+ const fetchUserRegistrations = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/events/my/registrations')
+ userRegistrations.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('User registrations endpoint not available, using mock data', error)
+ userRegistrations.value = generateMockRegistrations()
+ return { success: true, data: userRegistrations.value }
+ }
+ })
+ }
+
+ const registerForEvent = async (eventId, notes = '') => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.post('/events/register', {
+ event_id: eventId,
+ notes: notes
+ })
+
+ // Обновляем список регистраций
+ await fetchUserRegistrations()
+ return { success: true, data: response.data }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ const cancelRegistration = async (registrationId) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ await apiClient.delete(`/events/registrations/${registrationId}`)
+
+ // Обновляем список регистраций
+ await fetchUserRegistrations()
+ return { success: true }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ const checkEventAvailability = async (eventId) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get(`/events/${eventId}/availability`)
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.log(error)
+ console.warn('Event availability endpoint not available')
+ const event = events.value.find(e => e.id === parseInt(eventId))
+ const available = event &&
+ event.registration_open &&
+ (event.max_participants === 0 || event.participants_count < event.max_participants)
+ return { success: true, data: { available, remaining_spots: event.max_participants - event.participants_count } }
+ }
+ })
+ }
+
+ const createEvent = async (eventData) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.post('/events', eventData)
+ events.value.push(response.data)
+ return { success: true, data: response.data }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ const updateEvent = async (eventId, eventData) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.put(`/events/${eventId}`, eventData)
+ const index = events.value.findIndex(event => event.id === parseInt(eventId))
+ if (index !== -1) {
+ events.value[index] = response.data
+ }
+ return { success: true, data: response.data }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ // Вспомогательные функции для mock данных
+ const generateMockEvents = () => {
+ return [
+ {
+ id: 1,
+ title: 'Весенний марафон 2024',
+ description: 'Ежегодный весенний марафон по живописному маршруту через городской парк и набережную. Дистанции: 5км, 10км, полумарафон.',
+ date: '2024-04-15T09:00:00',
+ location: 'Городской парк, Центральный вход',
+ type: 'race',
+ distance: '5км, 10км, 21.1км',
+ participants_count: 45,
+ max_participants: 100,
+ registration_open: true,
+ image: '/images/spring-marathon.jpg',
+ created_at: '2024-01-15T10:00:00'
+ },
+ {
+ id: 2,
+ title: 'Тренировка для начинающих',
+ description: 'Групповая тренировка для тех, кто только начинает бегать. Основы техники, дыхания и планирования тренировок.',
+ date: '2024-03-20T18:30:00',
+ location: 'Стадион "Спартак"',
+ type: 'training',
+ distance: '',
+ participants_count: 12,
+ max_participants: 20,
+ registration_open: true,
+ image: '/images/beginner-training.jpg',
+ created_at: '2024-02-01T14:30:00'
+ },
+ {
+ id: 3,
+ title: 'Встреча бегового клуба',
+ description: 'Ежемесячная встреча участников бегового клуба. Обсуждение планов, обмен опытом, совместная пробежка.',
+ date: '2024-03-25T19:00:00',
+ location: 'Кафе "Бегун"',
+ type: 'social',
+ distance: '',
+ participants_count: 8,
+ max_participants: 0,
+ registration_open: true,
+ image: '/images/running-club.jpg',
+ created_at: '2024-02-10T11:20:00'
+ },
+ {
+ id: 4,
+ title: 'Семинар по питанию для бегунов',
+ description: 'Эксперт расскажет о правильном питании до, во время и после тренировок и соревнований.',
+ date: '2024-04-05T17:00:00',
+ location: 'Фитнес-центр "Актив"',
+ type: 'workshop',
+ distance: '',
+ participants_count: 25,
+ max_participants: 30,
+ registration_open: true,
+ image: '/images/nutrition-workshop.jpg',
+ created_at: '2024-01-20T16:45:00'
+ }
+ ]
+ }
+
+ const generateMockRegistrations = () => {
+ return [
+ {
+ id: 1,
+ user_id: 1,
+ event_id: 1,
+ status: 'confirmed',
+ notes: 'Участвую в полумарафоне',
+ result_time: '',
+ created_at: '2024-02-15T10:00:00',
+ event: {
+ id: 1,
+ title: 'Весенний марафон 2024',
+ date: '2024-04-15T09:00:00',
+ location: 'Городской парк'
+ }
+ },
+ {
+ id: 2,
+ user_id: 1,
+ event_id: 2,
+ status: 'pending',
+ notes: '',
+ result_time: '',
+ created_at: '2024-03-01T14:20:00',
+ event: {
+ id: 2,
+ title: 'Тренировка для начинающих',
+ date: '2024-03-20T18:30:00',
+ location: 'Стадион "Спартак"'
+ }
+ }
+ ]
+ }
+
+ const resetEventsStore = () => {
+ events.value = []
+ userRegistrations.value = []
+ eventDetails.value = null
+ loading.value = false
+ error.value = ''
+ }
+
+ return {
+ // State
+ events,
+ userRegistrations,
+ eventDetails,
+ loading,
+ error,
+
+ // Getters
+ upcomingEvents,
+ pastEvents,
+ registeredEvents,
+ pendingRegistrations,
+ eventTypes,
+
+ // Actions
+ fetchEvents,
+ fetchUpcomingEvents,
+ fetchEventDetails,
+ fetchUserRegistrations,
+ registerForEvent,
+ cancelRegistration,
+ checkEventAvailability,
+ createEvent,
+ updateEvent,
+ resetEventsStore
+ }
+})
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/stores/helpers/api.js b/main_dc/BB/bbvue/src/stores/helpers/api.js
index 1ca55dc..a8efad3 100644
--- a/main_dc/BB/bbvue/src/stores/helpers/api.js
+++ b/main_dc/BB/bbvue/src/stores/helpers/api.js
@@ -76,5 +76,6 @@ export const createLoadingHandler = (store) => {
}
}
+
export const api = apiClient;
export default apiClient;
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/stores/personalBests.js b/main_dc/BB/bbvue/src/stores/personalBests.js
new file mode 100644
index 0000000..62f0ae9
--- /dev/null
+++ b/main_dc/BB/bbvue/src/stores/personalBests.js
@@ -0,0 +1,245 @@
+// stores/personalBests.js
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { apiClient, withLoading } from './helpers/api'
+
+export const usePersonalBestsStore = defineStore('personalBests', () => {
+ // State
+ const personalBests = ref([])
+ const bestsSummary = ref(null)
+ const loading = ref(false)
+ const error = ref('')
+
+ // Getters
+ const verifiedBests = computed(() =>
+ personalBests.value.filter(best => best.verified)
+ )
+
+ const pendingBests = computed(() =>
+ personalBests.value.filter(best => !best.verified)
+ )
+
+ const bestsByDistance = computed(() => {
+ const grouped = {}
+ personalBests.value.forEach(best => {
+ if (!grouped[best.distance_type]) {
+ grouped[best.distance_type] = []
+ }
+ grouped[best.distance_type].push(best)
+ })
+ return grouped
+ })
+
+ const currentBests = computed(() => {
+ const bests = {}
+ personalBests.value.forEach(best => {
+ if (!bests[best.distance_type] ||
+ new Date(best.date) > new Date(bests[best.distance_type].date)) {
+ bests[best.distance_type] = best
+ }
+ })
+ return bests
+ })
+
+ const distanceLabels = {
+ '5k': '5 км',
+ '10k': '10 км',
+ 'half_marathon': 'Полумарафон (21.1 км)',
+ 'marathon': 'Марафон (42.2 км)',
+ 'other': 'Другая дистанция'
+ }
+
+ // Actions
+ const fetchPersonalBests = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/user/personal-bests')
+ personalBests.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Personal bests endpoint not available, using mock data', error)
+ personalBests.value = generateMockPersonalBests()
+ return { success: true, data: personalBests.value }
+ }
+ })
+ }
+
+ const fetchBestsSummary = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/user/personal-bests/summary')
+ bestsSummary.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Personal bests summary endpoint not available', error)
+ bestsSummary.value = generateBestsSummary()
+ return { success: true, data: bestsSummary.value }
+ }
+ })
+ }
+
+ const createPersonalBest = async (bestData) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.post('/user/personal-bests', bestData)
+ personalBests.value.push(response.data)
+ return { success: true, data: response.data }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ const updatePersonalBest = async (id, updateData) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.put(`/user/personal-bests/${id}`, updateData)
+ const index = personalBests.value.findIndex(best => best.id === id)
+ if (index !== -1) {
+ personalBests.value[index] = response.data
+ }
+ return { success: true, data: response.data }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ const deletePersonalBest = async (id) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ await apiClient.delete(`/user/personal-bests/${id}`)
+ personalBests.value = personalBests.value.filter(best => best.id !== id)
+ return { success: true }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ const verifyPersonalBest = async (id) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.patch(`/user/personal-bests/${id}/verify`)
+ const index = personalBests.value.findIndex(best => best.id === id)
+ if (index !== -1) {
+ personalBests.value[index] = response.data
+ }
+ return { success: true, data: response.data }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ const calculatePace = (distance, time) => {
+ // time в формате "HH:MM:SS" или "MM:SS"
+ const timeParts = time.split(':').map(part => parseInt(part))
+ let totalSeconds = 0
+
+ if (timeParts.length === 2) {
+ // MM:SS
+ totalSeconds = timeParts[0] * 60 + timeParts[1]
+ } else if (timeParts.length === 3) {
+ // HH:MM:SS
+ totalSeconds = timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]
+ }
+
+ const paceSecondsPerKm = totalSeconds / distance
+ const paceMinutes = Math.floor(paceSecondsPerKm / 60)
+ const paceSeconds = Math.round(paceSecondsPerKm % 60)
+
+ return `${paceMinutes}:${paceSeconds.toString().padStart(2, '0')}`
+ }
+
+ // Вспомогательные функции для mock данных
+ const generateMockPersonalBests = () => {
+ return [
+ {
+ id: 1,
+ distance_type: '5k',
+ time: '23:45',
+ pace: '4:45',
+ date: '2024-03-15',
+ verified: true,
+ event_name: 'Весенний забег',
+ location: 'Городской парк'
+ },
+ {
+ id: 2,
+ distance_type: '10k',
+ time: '48:15',
+ pace: '4:49',
+ date: '2024-02-20',
+ verified: true,
+ event_name: 'Зимний марафон',
+ location: 'Центральный стадион'
+ },
+ {
+ id: 3,
+ distance_type: 'half_marathon',
+ time: '1:48:30',
+ pace: '5:08',
+ date: '2024-01-10',
+ verified: true,
+ event_name: 'Новогодний полумарафон',
+ location: 'Набережная'
+ },
+ {
+ id: 4,
+ distance_type: '5k',
+ time: '22:30',
+ pace: '4:30',
+ date: '2024-03-20',
+ verified: false,
+ event_name: 'Тренировочный забег',
+ location: 'Лесопарк'
+ }
+ ]
+ }
+
+ const generateBestsSummary = () => {
+ return {
+ best_5k: '23:45',
+ best_5k_pace: '4:45',
+ best_10k: '48:15',
+ best_10k_pace: '4:49',
+ best_half_marathon: '1:48:30',
+ best_half_marathon_pace: '5:08',
+ best_marathon: null,
+ best_marathon_pace: null
+ }
+ }
+
+ const resetPersonalBestsStore = () => {
+ personalBests.value = []
+ bestsSummary.value = null
+ loading.value = false
+ error.value = ''
+ }
+
+ return {
+ // State
+ personalBests,
+ bestsSummary,
+ loading,
+ error,
+
+ // Getters
+ verifiedBests,
+ pendingBests,
+ bestsByDistance,
+ currentBests,
+ distanceLabels,
+
+ // Actions
+ fetchPersonalBests,
+ fetchBestsSummary,
+ createPersonalBest,
+ updatePersonalBest,
+ deletePersonalBest,
+ verifyPersonalBest,
+ calculatePace,
+ resetPersonalBestsStore
+ }
+})
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/stores/stats.js b/main_dc/BB/bbvue/src/stores/stats.js
new file mode 100644
index 0000000..b3df5f7
--- /dev/null
+++ b/main_dc/BB/bbvue/src/stores/stats.js
@@ -0,0 +1,285 @@
+// stores/stats.js
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { apiClient, withLoading } from './helpers/api'
+
+export const useStatsStore = defineStore('stats', () => {
+ // State
+ const detailedStats = ref(null)
+ const workoutHistory = ref([])
+ const monthlyStats = ref([])
+ const yearlyStats = ref([])
+ const paceAnalysis = ref(null)
+ const progressTrends = ref([])
+ const loading = ref(false)
+ const error = ref('')
+
+ // Getters
+ const totalWorkouts = computed(() => workoutHistory.value.length)
+
+ const totalDistance = computed(() =>
+ detailedStats.value?.total_distance ||
+ workoutHistory.value.reduce((sum, workout) => sum + (workout.distance_km || 0), 0)
+ )
+
+ const totalTime = computed(() =>
+ detailedStats.value?.total_time_min ||
+ workoutHistory.value.reduce((sum, workout) => sum + (workout.duration_min || 0), 0)
+ )
+
+ const averagePace = computed(() => {
+ if (totalDistance.value === 0 || totalTime.value === 0) return '0:00'
+
+ const totalMinutes = totalTime.value
+ const avgPaceMinPerKm = totalMinutes / totalDistance.value
+ const minutes = Math.floor(avgPaceMinPerKm)
+ const seconds = Math.round((avgPaceMinPerKm - minutes) * 60)
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`
+ })
+
+ const weeklyStats = computed(() => {
+ const last7Days = workoutHistory.value
+ .filter(workout => {
+ const workoutDate = new Date(workout.date)
+ const weekAgo = new Date()
+ weekAgo.setDate(weekAgo.getDate() - 7)
+ return workoutDate >= weekAgo
+ })
+
+ return {
+ workouts: last7Days.length,
+ distance: last7Days.reduce((sum, w) => sum + (w.distance_km || 0), 0),
+ time: last7Days.reduce((sum, w) => sum + (w.duration_min || 0), 0)
+ }
+ })
+
+ // Actions
+ const fetchDetailedStats = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/user/stats/detailed')
+ detailedStats.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Detailed stats endpoint not available, using fallback', error)
+ // Fallback на базовую статистику
+ return { success: true, data: detailedStats.value }
+ }
+ })
+ }
+
+ const fetchWorkoutHistory = async (params = {}) => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const queryString = new URLSearchParams(params).toString()
+ const response = await apiClient.get(`/user/workouts?${queryString}`)
+ workoutHistory.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Workout history endpoint not available, using mock data', error)
+ // Mock данные для демонстрации
+ workoutHistory.value = generateMockWorkoutHistory()
+ return { success: true, data: workoutHistory.value }
+ }
+ })
+ }
+
+ const fetchMonthlyStats = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/user/stats/monthly')
+ monthlyStats.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Monthly stats endpoint not available, generating from history', error)
+ monthlyStats.value = generateMonthlyStatsFromHistory()
+ return { success: true, data: monthlyStats.value }
+ }
+ })
+ }
+
+ const fetchYearlyStats = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/user/stats/yearly')
+ yearlyStats.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Yearly stats endpoint not available, generating from history', error)
+ yearlyStats.value = generateYearlyStatsFromHistory()
+ return { success: true, data: yearlyStats.value }
+ }
+ })
+ }
+
+ const fetchPaceAnalysis = async () => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get('/user/stats/pace-analysis')
+ paceAnalysis.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Pace analysis endpoint not available, generating from history', error)
+ paceAnalysis.value = generatePaceAnalysis()
+ return { success: true, data: paceAnalysis.value }
+ }
+ })
+ }
+
+ const fetchProgressTrends = async (period = '6months') => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get(`/user/stats/progress-trends?period=${period}`)
+ progressTrends.value = response.data
+ return { success: true, data: response.data }
+ } catch (error) {
+ console.warn('Progress trends endpoint not available, generating mock data', error)
+ progressTrends.value = generateProgressTrends(period)
+ return { success: true, data: progressTrends.value }
+ }
+ })
+ }
+
+ const exportStats = async (format = 'csv') => {
+ return withLoading({ loading, error }, async () => {
+ try {
+ const response = await apiClient.get(`/user/stats/export?format=${format}`, {
+ responseType: 'blob'
+ })
+ return { success: true, data: response.data }
+ } catch (error) {
+ return { success: false, error: error.message }
+ }
+ })
+ }
+
+ // Вспомогательные функции для mock данных
+ const generateMockWorkoutHistory = () => {
+ const types = ['easy', 'tempo', 'interval', 'long', 'recovery']
+ const history = []
+
+ for (let i = 0; i < 30; i++) {
+ const date = new Date()
+ date.setDate(date.getDate() - i * 2)
+
+ const type = types[Math.floor(Math.random() * types.length)]
+ const distance = type === 'long' ?
+ (8 + Math.random() * 12) :
+ (3 + Math.random() * 7)
+
+ const pace = type === 'interval' ? '4:30' :
+ type === 'tempo' ? '5:00' : '5:30'
+
+ history.push({
+ id: i + 1,
+ type,
+ distance_km: parseFloat(distance.toFixed(1)),
+ duration_min: Math.round(distance * (pace === '4:30' ? 4.5 : pace === '5:00' ? 5 : 5.5) * 60),
+ pace,
+ calories: Math.round(distance * 60),
+ date: date.toISOString().split('T')[0],
+ notes: i % 5 === 0 ? 'Хорошая тренировка' : ''
+ })
+ }
+
+ return history
+ }
+
+ const generateMonthlyStatsFromHistory = () => {
+ const months = []
+ for (let i = 0; i < 6; i++) {
+ const date = new Date()
+ date.setMonth(date.getMonth() - i)
+ months.push({
+ month: date.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' }),
+ distance_km: parseFloat((25 + Math.random() * 50).toFixed(1)),
+ workouts: Math.floor(8 + Math.random() * 12),
+ total_time_min: Math.floor(1500 + Math.random() * 3000)
+ })
+ }
+ return months.reverse()
+ }
+
+ const generateYearlyStatsFromHistory = () => {
+ const years = []
+ const currentYear = new Date().getFullYear()
+
+ for (let i = 0; i < 3; i++) {
+ years.push({
+ year: currentYear - i,
+ distance_km: parseFloat((300 + Math.random() * 500).toFixed(1)),
+ workouts: Math.floor(100 + Math.random() * 150),
+ total_time_min: Math.floor(18000 + Math.random() * 20000)
+ })
+ }
+ return years.reverse()
+ }
+
+ const generatePaceAnalysis = () => {
+ return {
+ easy: '5:45',
+ tempo: '5:15',
+ interval: '4:45',
+ long: '5:30',
+ recovery: '6:00',
+ overall_trend: 'improving',
+ best_pace: '4:30',
+ best_pace_date: '2024-03-15'
+ }
+ }
+
+ const generateProgressTrends = (period) => {
+ const trends = []
+ const points = period === '6months' ? 6 : 12
+
+ for (let i = 0; i < points; i++) {
+ trends.push({
+ period: `Период ${i + 1}`,
+ distance: parseFloat((20 + Math.random() * 30).toFixed(1)),
+ pace: parseFloat((5.0 + Math.random() * 1.0).toFixed(2)),
+ workouts: Math.floor(8 + Math.random() * 8)
+ })
+ }
+ return trends
+ }
+
+ const resetStatsStore = () => {
+ detailedStats.value = null
+ workoutHistory.value = []
+ monthlyStats.value = []
+ yearlyStats.value = []
+ paceAnalysis.value = null
+ progressTrends.value = []
+ loading.value = false
+ error.value = ''
+ }
+
+ return {
+ // State
+ detailedStats,
+ workoutHistory,
+ monthlyStats,
+ yearlyStats,
+ paceAnalysis,
+ progressTrends,
+ loading,
+ error,
+
+ // Getters
+ totalWorkouts,
+ totalDistance,
+ totalTime,
+ averagePace,
+ weeklyStats,
+
+ // Actions
+ fetchDetailedStats,
+ fetchWorkoutHistory,
+ fetchMonthlyStats,
+ fetchYearlyStats,
+ fetchPaceAnalysis,
+ fetchProgressTrends,
+ exportStats,
+ resetStatsStore
+ }
+})
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/views/DetailedStats.vue b/main_dc/BB/bbvue/src/views/DetailedStats.vue
new file mode 100644
index 0000000..4cb8d4c
--- /dev/null
+++ b/main_dc/BB/bbvue/src/views/DetailedStats.vue
@@ -0,0 +1,525 @@
+
+
+
+
+
Загрузка статистики...
+
+
+
+
+
Общая сводка
+
+
+
🏃 Всего пробег
+
{{ totalDistance }} км
+
+
+
⏱️ Общее время
+
{{ formatTime(totalTime) }}
+
+
+
📅 Тренировок
+
{{ totalWorkouts }}
+
+
+
⚡ Средний темп
+
{{ averagePace }}/км
+
+
+
+
+
+
+
📅 Статистика за неделю
+
+
+ Пробег:
+ {{ weeklyStats.distance }} км
+
+
+ Тренировки:
+ {{ weeklyStats.workouts }}
+
+
+ Время:
+ {{ formatTime(weeklyStats.time) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Дистанция:
+ {{ workout.distance_km }} км
+
+
+ Время:
+ {{ formatTime(workout.duration_min) }}
+
+
+ Темп:
+ {{ workout.pace }}/км
+
+
+ Калории:
+ {{ workout.calories }}
+
+
+
+ {{ workout.notes }}
+
+
+
+
+
+
+
+
📊 Ежемесячная статистика
+
+
+
{{ month.month }}
+
+ {{ month.distance_km }} км
+ {{ month.workouts }} тренировок
+
+
+
+
+
+
+
+
⚡ Анализ темпа
+
+
+ Легкие:
+ {{ paceAnalysis.easy }}/км
+
+
+ Темповые:
+ {{ paceAnalysis.tempo }}/км
+
+
+ Интервалы:
+ {{ paceAnalysis.interval }}/км
+
+
+ Длинные:
+ {{ paceAnalysis.long }}/км
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/views/Events.vue b/main_dc/BB/bbvue/src/views/Events.vue
new file mode 100644
index 0000000..e6ea189
--- /dev/null
+++ b/main_dc/BB/bbvue/src/views/Events.vue
@@ -0,0 +1,426 @@
+
+
+
+
+
+
+
🔮 Предстоящие события
+
+
Нет предстоящих событий
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ selectedEvent.title }}
+
+
+ 📅
+ {{ formatDate(selectedEvent.date) }}
+
+
+ 📍
+ {{ selectedEvent.location }}
+
+
+ 🏃
+ {{ selectedEvent.distance }}
+
+
+
+
+
+
+ ✅ Есть свободные места
+
+ (осталось: {{ availabilityCheck.remaining_spots }})
+
+
+
+ ❌ Регистрация закрыта или нет свободных мест
+
+
+
+
+
+
+
+
+
+
+
+ ✅ Вы успешно зарегистрировались на событие!
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/views/PersonalBests.vue b/main_dc/BB/bbvue/src/views/PersonalBests.vue
new file mode 100644
index 0000000..7d82c2e
--- /dev/null
+++ b/main_dc/BB/bbvue/src/views/PersonalBests.vue
@@ -0,0 +1,693 @@
+
+
+
+
+
Загрузка рекордов...
+
+
+
+
+
🏆 Лучшие результаты
+
+
+
{{ distanceLabels[distance] }}
+
{{ best.time }}
+
Темп: {{ best.pace }}/км
+
{{ formatDate(best.date) }}
+
+
+
+
+
+
+
✅ Подтвержденные рекорды
+
+
+
+
+
{{ best.time }}
+
Темп: {{ best.pace }}/км
+
{{ best.event_name }}
+
+ {{ formatDate(best.date) }}
+ • {{ best.location }}
+
+
+
+
+
+
+
+
+
+
+
+
+
⏳ Ожидают подтверждения
+
+
+
+
+
{{ best.time }}
+
Темп: {{ best.pace }}/км
+
{{ best.event_name }}
+
+ {{ formatDate(best.date) }}
+ • {{ best.location }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
✏️ Редактировать рекорд
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main_dc/BB/bbvue/src/views/Profile.vue b/main_dc/BB/bbvue/src/views/Profile.vue
index 002fd07..1a1b89f 100644
--- a/main_dc/BB/bbvue/src/views/Profile.vue
+++ b/main_dc/BB/bbvue/src/views/Profile.vue
@@ -176,7 +176,7 @@
-
+
+
+