201 előtti mentés
This commit is contained in:
202
frontend/src/stores/analyticsStore.js
Normal file
202
frontend/src/stores/analyticsStore.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAuthStore } from './authStore'
|
||||
|
||||
export const useAnalyticsStore = defineStore('analytics', () => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Real data - initially empty, will be fetched from API
|
||||
const monthlyCosts = ref([])
|
||||
const fuelEfficiencyTrends = ref([])
|
||||
const costPerKmTrends = ref([])
|
||||
const funFacts = ref({
|
||||
totalKmDriven: 0,
|
||||
totalTreesSaved: 0,
|
||||
totalCo2Saved: 0,
|
||||
totalMoneySaved: 0,
|
||||
moonTrips: computed(() => Math.round(funFacts.value.totalKmDriven / 384400)),
|
||||
earthCircuits: computed(() => Math.round(funFacts.value.totalKmDriven / 40075)),
|
||||
})
|
||||
const businessMetrics = ref({
|
||||
fleetSize: 0,
|
||||
averageVehicleAge: 0,
|
||||
totalMonthlyCost: 0,
|
||||
averageCostPerKm: 0,
|
||||
utilizationRate: 0,
|
||||
downtimeHours: 0,
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Getters
|
||||
const totalCosts = computed(() => {
|
||||
return monthlyCosts.value.reduce((sum, month) => sum + month.total, 0)
|
||||
})
|
||||
|
||||
const averageMonthlyCost = computed(() => {
|
||||
return monthlyCosts.value.length > 0 ? totalCosts.value / monthlyCosts.value.length : 0
|
||||
})
|
||||
|
||||
const averageFuelEfficiency = computed(() => {
|
||||
const sum = fuelEfficiencyTrends.value.reduce((acc, item) => acc + item.efficiency, 0)
|
||||
return fuelEfficiencyTrends.value.length > 0 ? sum / fuelEfficiencyTrends.value.length : 0
|
||||
})
|
||||
|
||||
const averageCostPerKm = computed(() => {
|
||||
const sum = costPerKmTrends.value.reduce((acc, item) => acc + item.cost, 0)
|
||||
return costPerKmTrends.value.length > 0 ? sum / costPerKmTrends.value.length : 0
|
||||
})
|
||||
|
||||
// Actions
|
||||
function addMonthlyCost(data) {
|
||||
monthlyCosts.value.push(data)
|
||||
}
|
||||
|
||||
function updateFuelEfficiency(month, efficiency) {
|
||||
const index = fuelEfficiencyTrends.value.findIndex(item => item.month === month)
|
||||
if (index !== -1) {
|
||||
fuelEfficiencyTrends.value[index].efficiency = efficiency
|
||||
}
|
||||
}
|
||||
|
||||
function updateFunFacts(newFacts) {
|
||||
Object.assign(funFacts.value, newFacts)
|
||||
}
|
||||
|
||||
// Real API fetch - NO MORE MOCK DATA
|
||||
async function fetchDashboardAnalytics() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// Get auth token
|
||||
const token = authStore.token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
// Call real backend API
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/analytics/dashboard`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch analytics: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AnalyticsStore: Fetched dashboard analytics', data)
|
||||
|
||||
// Transform API response to frontend format
|
||||
monthlyCosts.value = data.monthly_costs || []
|
||||
fuelEfficiencyTrends.value = data.fuel_efficiency_trends || []
|
||||
costPerKmTrends.value = data.cost_per_km_trends || []
|
||||
|
||||
if (data.fun_facts) {
|
||||
funFacts.value = {
|
||||
totalKmDriven: data.fun_facts.total_km_driven || 0,
|
||||
totalTreesSaved: data.fun_facts.total_trees_saved || 0,
|
||||
totalCo2Saved: data.fun_facts.total_co2_saved || 0,
|
||||
totalMoneySaved: data.fun_facts.total_money_saved || 0,
|
||||
moonTrips: computed(() => Math.round((data.fun_facts.total_km_driven || 0) / 384400)),
|
||||
earthCircuits: computed(() => Math.round((data.fun_facts.total_km_driven || 0) / 40075)),
|
||||
}
|
||||
}
|
||||
|
||||
if (data.business_metrics) {
|
||||
businessMetrics.value = {
|
||||
fleetSize: data.business_metrics.fleet_size || 0,
|
||||
averageVehicleAge: data.business_metrics.average_vehicle_age || 0,
|
||||
totalMonthlyCost: data.business_metrics.total_monthly_cost || 0,
|
||||
averageCostPerKm: data.business_metrics.average_cost_per_km || 0,
|
||||
utilizationRate: data.business_metrics.utilization_rate || 0,
|
||||
downtimeHours: data.business_metrics.downtime_hours || 0,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AnalyticsStore: Error fetching analytics', err)
|
||||
error.value = err.message
|
||||
// Keep empty data (no mock fallback)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch vehicle-specific analytics
|
||||
async function fetchVehicleAnalytics(vehicleId) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const token = authStore.token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
// Call vehicle summary endpoint
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/analytics/${vehicleId}/summary`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch vehicle analytics: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AnalyticsStore: Fetched vehicle analytics', data)
|
||||
|
||||
// For now, just return the data - frontend components can use it directly
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AnalyticsStore: Error fetching vehicle analytics', err)
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fleet analytics (aggregated)
|
||||
async function fetchFleetAnalytics() {
|
||||
// For now, use the dashboard endpoint which includes fleet metrics
|
||||
return fetchDashboardAnalytics()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
monthlyCosts,
|
||||
fuelEfficiencyTrends,
|
||||
costPerKmTrends,
|
||||
funFacts,
|
||||
businessMetrics,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Getters
|
||||
totalCosts,
|
||||
averageMonthlyCost,
|
||||
averageFuelEfficiency,
|
||||
averageCostPerKm,
|
||||
|
||||
// Actions
|
||||
addMonthlyCost,
|
||||
updateFuelEfficiency,
|
||||
updateFunFacts,
|
||||
fetchDashboardAnalytics,
|
||||
fetchVehicleAnalytics,
|
||||
fetchFleetAnalytics,
|
||||
}
|
||||
})
|
||||
103
frontend/src/stores/appModeStore.js
Normal file
103
frontend/src/stores/appModeStore.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
export const useAppModeStore = defineStore('appMode', () => {
|
||||
// State
|
||||
const mode = ref('personal') // backend compatible values: 'personal' or 'fleet'
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Getters
|
||||
const isPrivateGarage = computed(() => mode.value === 'personal')
|
||||
const isCorporateFleet = computed(() => mode.value === 'fleet')
|
||||
|
||||
// Actions
|
||||
async function setMode(newMode) {
|
||||
// Map UI values to backend compatible values
|
||||
let backendMode = newMode
|
||||
if (newMode === 'private_garage') {
|
||||
backendMode = 'personal'
|
||||
} else if (newMode === 'corporate_fleet') {
|
||||
backendMode = 'fleet'
|
||||
}
|
||||
|
||||
if (!['personal', 'fleet'].includes(backendMode)) {
|
||||
console.error('Invalid mode:', newMode)
|
||||
return
|
||||
}
|
||||
mode.value = backendMode
|
||||
persistMode(backendMode)
|
||||
await saveModeToBackend(backendMode)
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
const newMode = mode.value === 'personal' ? 'fleet' : 'personal'
|
||||
setMode(newMode)
|
||||
}
|
||||
|
||||
// SSR-safe localStorage persistence
|
||||
function persistMode(mode) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('ui_mode', mode)
|
||||
}
|
||||
}
|
||||
|
||||
function getInitialMode() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('ui_mode')
|
||||
// Map UI values to backend compatible values
|
||||
if (saved === 'private_garage' || saved === 'personal') {
|
||||
return 'personal'
|
||||
} else if (saved === 'corporate_fleet' || saved === 'fleet') {
|
||||
return 'fleet'
|
||||
}
|
||||
}
|
||||
// Default mode
|
||||
return 'personal'
|
||||
}
|
||||
|
||||
// Load user preferences from backend on app startup
|
||||
async function loadModeFromBackend() {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await api.get('/api/v1/users/me')
|
||||
const user = response.data
|
||||
if (user.ui_mode && ['personal', 'fleet'].includes(user.ui_mode)) {
|
||||
mode.value = user.ui_mode
|
||||
persistMode(user.ui_mode)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load UI mode from backend, using local storage', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Save mode to backend via PATCH /users/me/preferences
|
||||
async function saveModeToBackend(newMode) {
|
||||
try {
|
||||
await api.patch('/api/v1/users/me/preferences', {
|
||||
ui_mode: newMode
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to save UI mode to backend', error)
|
||||
// Optionally revert local state? For now just log.
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
mode.value = getInitialMode()
|
||||
// Load from backend after store creation (call in component's onMounted)
|
||||
// We expose loadModeFromBackend for components to call
|
||||
|
||||
return {
|
||||
mode,
|
||||
isLoading,
|
||||
isPrivateGarage,
|
||||
isCorporateFleet,
|
||||
setMode,
|
||||
toggleMode,
|
||||
loadModeFromBackend,
|
||||
}
|
||||
})
|
||||
@@ -1,10 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import router from '../router'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const router = useRouter()
|
||||
|
||||
// State
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const isAdmin = ref(localStorage.getItem('is_admin') === 'true')
|
||||
@@ -33,7 +31,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
params.append('password', password)
|
||||
|
||||
// Call real backend API
|
||||
const response = await fetch('http://localhost:8000/api/v1/auth/login', {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
@@ -60,16 +59,16 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
// We need to decode the JWT token to get user role and info
|
||||
// For now, we'll make a separate API call to get user info
|
||||
// Or we can parse the JWT token (simple base64 decode)
|
||||
let userRole = 'user'
|
||||
let isAdmin = false
|
||||
let roleValue = 'user'
|
||||
let adminFlag = false
|
||||
|
||||
try {
|
||||
// Decode JWT token to get payload
|
||||
const tokenParts = accessToken.split('.')
|
||||
if (tokenParts.length === 3) {
|
||||
const payload = JSON.parse(atob(tokenParts[1]))
|
||||
userRole = payload.role || 'user'
|
||||
isAdmin = userRole === 'admin' || userRole === 'superadmin'
|
||||
roleValue = payload.role || 'user'
|
||||
adminFlag = roleValue === 'admin' || roleValue === 'superadmin'
|
||||
console.log('AuthStore: Decoded JWT payload', payload)
|
||||
}
|
||||
} catch (decodeError) {
|
||||
@@ -81,15 +80,15 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
// Save to localStorage
|
||||
localStorage.setItem('token', accessToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
localStorage.setItem('is_admin', isAdmin.toString())
|
||||
localStorage.setItem('is_admin', adminFlag.toString())
|
||||
localStorage.setItem('user_email', email)
|
||||
localStorage.setItem('user_role', userRole)
|
||||
localStorage.setItem('user_role', roleValue)
|
||||
|
||||
// Update store state
|
||||
token.value = accessToken
|
||||
isAdmin.value = isAdmin
|
||||
isAdmin.value = adminFlag
|
||||
userEmail.value = email
|
||||
userRole.value = userRole
|
||||
userRole.value = roleValue
|
||||
|
||||
console.log('AuthStore: State updated, redirecting to /profile-select')
|
||||
|
||||
@@ -102,49 +101,11 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
throw error
|
||||
}
|
||||
|
||||
return { success: true, token: accessToken, isAdmin, role: userRole }
|
||||
return { success: true, token: accessToken, isAdmin: adminFlag, role: roleValue }
|
||||
|
||||
} catch (error) {
|
||||
console.error('AuthStore: Login failed', error)
|
||||
|
||||
// Fallback to mock login for development if API is not available
|
||||
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
|
||||
console.warn('AuthStore: API unavailable, falling back to mock login for development')
|
||||
|
||||
// Determine user role based on email
|
||||
let mockIsAdmin = false
|
||||
let mockRole = 'user'
|
||||
|
||||
if (email === 'superadmin@profibot.hu') {
|
||||
mockIsAdmin = true
|
||||
mockRole = 'admin'
|
||||
} else if (email === 'tester_pro@profibot.hu') {
|
||||
mockIsAdmin = true // Tester has admin privileges for testing
|
||||
mockRole = 'tester'
|
||||
}
|
||||
|
||||
// Set mock token
|
||||
const mockToken = 'mock_jwt_token_' + Date.now()
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('token', mockToken)
|
||||
localStorage.setItem('is_admin', mockIsAdmin.toString())
|
||||
localStorage.setItem('user_email', email)
|
||||
localStorage.setItem('user_role', mockRole)
|
||||
|
||||
// Update store state
|
||||
token.value = mockToken
|
||||
isAdmin.value = mockIsAdmin
|
||||
userEmail.value = email
|
||||
userRole.value = mockRole
|
||||
|
||||
// Redirect
|
||||
await router.push('/profile-select')
|
||||
|
||||
return { success: true, token: mockToken, isAdmin: mockIsAdmin, role: mockRole }
|
||||
}
|
||||
|
||||
throw error
|
||||
throw error // Re-throw the error instead of falling back to mock
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +134,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/api/v1/users/me', {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/api/v1/users/me`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
|
||||
28
frontend/src/stores/expenseStore.js
Normal file
28
frontend/src/stores/expenseStore.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
export const useExpenseStore = defineStore('expense', () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
async function createExpense(expenseData) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.post('/api/v1/expenses/', expenseData)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.detail || err.message
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
createExpense,
|
||||
}
|
||||
})
|
||||
158
frontend/src/stores/gamificationStore.js
Normal file
158
frontend/src/stores/gamificationStore.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
export const useGamificationStore = defineStore('gamification', () => {
|
||||
// State
|
||||
const achievements = ref([])
|
||||
const badges = ref([])
|
||||
const userStats = ref(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Getters
|
||||
const earnedAchievements = computed(() =>
|
||||
achievements.value.filter(a => a.is_earned)
|
||||
)
|
||||
|
||||
const lockedAchievements = computed(() =>
|
||||
achievements.value.filter(a => !a.is_earned)
|
||||
)
|
||||
|
||||
const totalAchievements = computed(() => achievements.value.length)
|
||||
|
||||
const earnedCount = computed(() => earnedAchievements.value.length)
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
if (totalAchievements.value === 0) return 0
|
||||
return Math.round((earnedCount.value / totalAchievements.value) * 100)
|
||||
})
|
||||
|
||||
const earnedBadges = computed(() =>
|
||||
badges.value.filter(b => b.is_earned)
|
||||
)
|
||||
|
||||
// Helper function for API calls (using centralized api instance)
|
||||
async function apiFetch(url, options = {}) {
|
||||
try {
|
||||
const response = await api.get(url, options)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
throw new Error(`API error ${error.response.status}: ${JSON.stringify(error.response.data)}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function fetchAchievements() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/achievements')
|
||||
achievements.value = data.achievements || []
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch achievements:', err)
|
||||
error.value = err.message
|
||||
// No mock fallback - let the error propagate
|
||||
achievements.value = []
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBadges() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/my-badges')
|
||||
badges.value = data.map(badge => ({
|
||||
id: badge.badge_id,
|
||||
title: badge.badge_name,
|
||||
description: badge.badge_description,
|
||||
icon_url: badge.badge_icon_url,
|
||||
is_earned: true,
|
||||
earned_date: badge.earned_at,
|
||||
category: 'badge'
|
||||
}))
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch badges:', err)
|
||||
error.value = err.message
|
||||
// No mock fallback - propagate error
|
||||
badges.value = []
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserStats() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/me')
|
||||
userStats.value = data
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user stats:', err)
|
||||
error.value = err.message
|
||||
// No mock fallback - propagate error
|
||||
userStats.value = null
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAllGamificationData() {
|
||||
await Promise.all([
|
||||
fetchAchievements(),
|
||||
fetchBadges(),
|
||||
fetchUserStats()
|
||||
])
|
||||
}
|
||||
|
||||
async function earnAchievement(id) {
|
||||
// In a real implementation, this would call an API endpoint
|
||||
// For now, we'll just update local state
|
||||
const achievement = achievements.value.find(a => a.id === id)
|
||||
if (achievement && !achievement.is_earned) {
|
||||
achievement.is_earned = true
|
||||
achievement.earned_date = new Date().toISOString().split('T')[0]
|
||||
}
|
||||
}
|
||||
|
||||
function resetAchievements() {
|
||||
achievements.value.forEach(a => {
|
||||
a.is_earned = false
|
||||
a.earned_date = null
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize store with data - RE-ENABLED after token fix
|
||||
fetchAllGamificationData()
|
||||
|
||||
return {
|
||||
achievements,
|
||||
badges,
|
||||
userStats,
|
||||
earnedAchievements,
|
||||
lockedAchievements,
|
||||
totalAchievements,
|
||||
earnedCount,
|
||||
progressPercentage,
|
||||
earnedBadges,
|
||||
isLoading,
|
||||
error,
|
||||
fetchAchievements,
|
||||
fetchBadges,
|
||||
fetchUserStats,
|
||||
fetchAllGamificationData,
|
||||
earnAchievement,
|
||||
resetAchievements
|
||||
}
|
||||
})
|
||||
@@ -30,14 +30,15 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
|
||||
try {
|
||||
// Transform frontend vehicle data to API schema
|
||||
// For draft vehicles (2-step creation), VIN can be null
|
||||
const payload = {
|
||||
vin: vehicle.vin || `TEMP${Date.now()}`,
|
||||
vin: vehicle.vin || null, // Send null for draft vehicles
|
||||
license_plate: vehicle.licensePlate || 'N/A',
|
||||
catalog_id: vehicle.catalogId || null,
|
||||
organization_id: vehicle.organizationId || 1 // Default org ID
|
||||
}
|
||||
|
||||
const response = await fetch('http://localhost:8000/api/v1/assets/vehicles', {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/assets/vehicles`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
@@ -55,11 +56,10 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
const data = await response.json()
|
||||
console.log('GarageStore: Vehicle created successfully', data)
|
||||
|
||||
// Transform API response to frontend format and add to local state
|
||||
const transformedVehicle = transformApiResponse(data)[0]
|
||||
vehicles.value.push(transformedVehicle)
|
||||
// After successful save, fetch fresh data from server to ensure consistency
|
||||
await fetchVehicles()
|
||||
|
||||
return transformedVehicle
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('GarageStore: Error adding vehicle', err)
|
||||
throw err
|
||||
@@ -98,7 +98,7 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
|
||||
// Call real backend API
|
||||
// First try the assets endpoint for user's vehicles
|
||||
const response = await fetch('http://localhost:8000/api/v1/assets/vehicles', {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/assets/vehicles`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
@@ -110,7 +110,7 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
// If 404, try alternative endpoint
|
||||
if (response.status === 404) {
|
||||
// Try user assets endpoint
|
||||
const userResponse = await fetch('http://localhost:8000/api/v1/users/me/assets', {
|
||||
const userResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/users/me/assets`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
||||
261
frontend/src/stores/quizStore.js
Normal file
261
frontend/src/stores/quizStore.js
Normal file
@@ -0,0 +1,261 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useQuizStore = defineStore('quiz', () => {
|
||||
// State
|
||||
const userPoints = ref(0)
|
||||
const currentStreak = ref(0)
|
||||
const lastPlayedDate = ref(null)
|
||||
const questions = ref([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Getters
|
||||
const canPlayToday = computed(() => {
|
||||
if (!lastPlayedDate.value) return true
|
||||
const last = new Date(lastPlayedDate.value)
|
||||
const now = new Date()
|
||||
// Reset at midnight (different calendar day)
|
||||
const lastDay = last.toDateString()
|
||||
const today = now.toDateString()
|
||||
return lastDay !== today
|
||||
})
|
||||
|
||||
const totalQuestions = computed(() => questions.value.length)
|
||||
|
||||
// Helper function to get auth token
|
||||
function getAuthToken() {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Try both token keys for compatibility
|
||||
return localStorage.getItem('token') || localStorage.getItem('auth_token')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Helper function for API calls
|
||||
async function apiFetch(url, options = {}) {
|
||||
const token = getAuthToken()
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}${url}`, {
|
||||
...options,
|
||||
headers
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API error ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function fetchQuizStats() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/quiz/stats')
|
||||
userPoints.value = data.total_quiz_points || 0
|
||||
currentStreak.value = data.current_streak || 0
|
||||
lastPlayedDate.value = data.last_played || null
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch quiz stats:', err)
|
||||
error.value = err.message
|
||||
// Fallback to localStorage if API fails
|
||||
userPoints.value = getStoredPoints()
|
||||
currentStreak.value = getStoredStreak()
|
||||
lastPlayedDate.value = getStoredLastPlayedDate()
|
||||
return {
|
||||
total_quiz_points: userPoints.value,
|
||||
current_streak: currentStreak.value,
|
||||
last_played: lastPlayedDate.value,
|
||||
can_play_today: canPlayToday.value
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDailyQuiz() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/quiz/daily')
|
||||
questions.value = data.questions || []
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch daily quiz:', err)
|
||||
error.value = err.message
|
||||
// Fallback to mock questions if API fails
|
||||
questions.value = getMockQuestions()
|
||||
return {
|
||||
questions: questions.value,
|
||||
total_questions: questions.value.length,
|
||||
date: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function answerQuestion(questionId, selectedOptionIndex) {
|
||||
try {
|
||||
const response = await apiFetch('/api/v1/gamification/quiz/answer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
question_id: questionId,
|
||||
selected_option: selectedOptionIndex
|
||||
})
|
||||
})
|
||||
|
||||
if (response.is_correct) {
|
||||
userPoints.value += response.points_awarded
|
||||
currentStreak.value += 1
|
||||
persistState() // Update localStorage as fallback
|
||||
} else {
|
||||
currentStreak.value = 0
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error('Failed to submit quiz answer:', err)
|
||||
error.value = err.message
|
||||
// Fallback to local logic
|
||||
return answerQuestionLocal(questionId, selectedOptionIndex)
|
||||
}
|
||||
}
|
||||
|
||||
async function completeDailyQuiz() {
|
||||
try {
|
||||
await apiFetch('/api/v1/gamification/quiz/complete', {
|
||||
method: 'POST'
|
||||
})
|
||||
lastPlayedDate.value = new Date().toISOString()
|
||||
persistState()
|
||||
} catch (err) {
|
||||
console.error('Failed to complete daily quiz:', err)
|
||||
error.value = err.message
|
||||
// Fallback to local storage
|
||||
lastPlayedDate.value = new Date().toISOString()
|
||||
persistState()
|
||||
}
|
||||
}
|
||||
|
||||
// Local fallback functions
|
||||
function answerQuestionLocal(questionId, selectedOptionIndex) {
|
||||
const question = questions.value.find(q => q.id === questionId)
|
||||
if (!question) return { is_correct: false, correct_answer: -1, explanation: 'Question not found' }
|
||||
|
||||
const isCorrect = selectedOptionIndex === question.correctAnswer
|
||||
if (isCorrect) {
|
||||
userPoints.value += 10
|
||||
currentStreak.value += 1
|
||||
} else {
|
||||
currentStreak.value = 0
|
||||
}
|
||||
|
||||
persistState()
|
||||
return {
|
||||
is_correct: isCorrect,
|
||||
correct_answer: question.correctAnswer,
|
||||
points_awarded: isCorrect ? 10 : 0,
|
||||
explanation: question.explanation
|
||||
}
|
||||
}
|
||||
|
||||
function resetStreak() {
|
||||
currentStreak.value = 0
|
||||
persistState()
|
||||
}
|
||||
|
||||
function addPoints(points) {
|
||||
userPoints.value += points
|
||||
persistState()
|
||||
}
|
||||
|
||||
// SSR-safe localStorage persistence (fallback only)
|
||||
function persistState() {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('quiz_points', userPoints.value.toString())
|
||||
localStorage.setItem('quiz_streak', currentStreak.value.toString())
|
||||
localStorage.setItem('quiz_last_played', lastPlayedDate.value)
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredPoints() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return parseInt(localStorage.getItem('quiz_points') || '0')
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function getStoredStreak() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return parseInt(localStorage.getItem('quiz_streak') || '0')
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function getStoredLastPlayedDate() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('quiz_last_played') || null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getMockQuestions() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
question: 'Melyik alkatrész felelős a motor levegő‑üzemanyag keverékének szabályozásáért?',
|
||||
options: ['Generátor', 'Lambda‑szonda', 'Féktárcsa', 'Olajszűrő'],
|
||||
correctAnswer: 1,
|
||||
explanation: 'A lambda‑szonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: 'Mennyi ideig érvényes egy gépjármű műszaki vizsgája Magyarországon?',
|
||||
options: ['1 év', '2 év', '4 év', '6 év'],
|
||||
correctAnswer: 1,
|
||||
explanation: 'A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: 'Melyik anyag NEM része a hibrid autók akkumulátorának?',
|
||||
options: ['Lítium', 'Nikkel', 'Ólom', 'Kobalt'],
|
||||
correctAnswer: 2,
|
||||
explanation: 'A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólom‑savas akkukban van.'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Initialize store with stats - DISABLED for debugging
|
||||
// fetchQuizStats()
|
||||
console.log('🚨 Quiz store: Auto-fetch DISABLED for debugging')
|
||||
|
||||
return {
|
||||
userPoints,
|
||||
currentStreak,
|
||||
lastPlayedDate,
|
||||
questions,
|
||||
canPlayToday,
|
||||
totalQuestions,
|
||||
isLoading,
|
||||
error,
|
||||
fetchQuizStats,
|
||||
fetchDailyQuiz,
|
||||
answerQuestion,
|
||||
completeDailyQuiz,
|
||||
resetStreak,
|
||||
addPoints
|
||||
}
|
||||
})
|
||||
43
frontend/src/stores/themeStore.js
Normal file
43
frontend/src/stores/themeStore.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useThemeStore = defineStore('theme', {
|
||||
state: () => ({
|
||||
currentTheme: 'luxury_showroom', // 'luxury_showroom' or 'rusty_workshop'
|
||||
}),
|
||||
getters: {
|
||||
isLuxury: (state) => state.currentTheme === 'luxury_showroom',
|
||||
isWorkshop: (state) => state.currentTheme === 'rusty_workshop',
|
||||
themeClasses: (state) => {
|
||||
if (state.currentTheme === 'luxury_showroom') {
|
||||
return {
|
||||
background: 'bg-gradient-to-br from-slate-900 via-slate-800 to-gray-900',
|
||||
text: 'text-amber-100',
|
||||
accent: 'text-amber-400',
|
||||
border: 'border-amber-700',
|
||||
card: 'bg-slate-800/70 backdrop-blur-lg border border-amber-700/30',
|
||||
button: 'bg-amber-700 hover:bg-amber-600 text-white',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
background: 'bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900',
|
||||
text: 'text-orange-100',
|
||||
accent: 'text-orange-400',
|
||||
border: 'border-orange-800',
|
||||
card: 'bg-gray-800/90 border border-dashed border-orange-700/50',
|
||||
button: 'bg-orange-800 hover:bg-orange-700 text-white',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleTheme() {
|
||||
this.currentTheme = this.currentTheme === 'luxury_showroom' ? 'rusty_workshop' : 'luxury_showroom'
|
||||
},
|
||||
setTheme(theme) {
|
||||
if (['luxury_showroom', 'rusty_workshop'].includes(theme)) {
|
||||
this.currentTheme = theme
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: true, // optional: if using pinia-plugin-persistedstate
|
||||
})
|
||||
Reference in New Issue
Block a user