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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@
- +
+ +