2026.03.29 20:00 Gitea_manager javítás előtt
This commit is contained in:
2
frontend/admin/.nuxt/imports.d.ts
vendored
2
frontend/admin/.nuxt/imports.d.ts
vendored
@@ -30,7 +30,7 @@ export { requestIdleCallback, cancelIdleCallback } from '#app/compat/idle-callba
|
||||
export { setInterval } from '#app/compat/interval';
|
||||
export { definePageMeta } from '../node_modules/nuxt/dist/pages/runtime/composables';
|
||||
export { defineLazyHydrationComponent } from '#app/composables/lazy-hydration';
|
||||
export { default as useHealthMonitor, HealthMetrics, SystemAlert, HealthMonitorState } from '../composables/useHealthMonitor';
|
||||
export { useHealthMonitor, HealthMetrics, SystemAlert, HealthMonitorState } from '../composables/useHealthMonitor';
|
||||
export { default as usePolling, PollingOptions, PollingState } from '../composables/usePolling';
|
||||
export { Role, Role, ScopeLevel, ScopeLevel, RoleRank, AdminTiles, useRBAC, TilePermission } from '../composables/useRBAC';
|
||||
export { useServiceMap, Service, Scope } from '../composables/useServiceMap';
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"id":"dev","timestamp":1774433357734}
|
||||
{"id":"dev","timestamp":1774557833950}
|
||||
@@ -1 +1 @@
|
||||
{"id":"dev","timestamp":1774433357734,"prerendered":[]}
|
||||
{"id":"dev","timestamp":1774557833950,"prerendered":[]}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"date": "2026-03-25T10:09:22.800Z",
|
||||
"date": "2026-03-26T20:43:59.681Z",
|
||||
"preset": "nitro-dev",
|
||||
"framework": {
|
||||
"name": "nuxt",
|
||||
@@ -11,7 +11,7 @@
|
||||
"dev": {
|
||||
"pid": 19,
|
||||
"workerAddress": {
|
||||
"socketPath": "\u0000nitro-worker-19-1-1-2130.sock"
|
||||
"socketPath": "\u0000nitro-worker-19-1-1-9144.sock"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
frontend/admin/.nuxt/nuxt.d.ts
vendored
4
frontend/admin/.nuxt/nuxt.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
/// <reference types="@nuxtjs/tailwindcss" />
|
||||
/// <reference types="@pinia/nuxt" />
|
||||
/// <reference types="vuetify-nuxt-module" />
|
||||
/// <reference types="@nuxtjs/i18n" />
|
||||
/// <reference types="@pinia/nuxt" />
|
||||
/// <reference types="@nuxt/telemetry" />
|
||||
/// <reference types="@nuxtjs/tailwindcss" />
|
||||
/// <reference path="types/nitro-layouts.d.ts" />
|
||||
/// <reference path="types/builder-env.d.ts" />
|
||||
/// <reference types="nuxt" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 3/25/2026, 8:30:35 PM
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 3/27/2026, 9:42:29 AM
|
||||
import "@nuxtjs/tailwindcss/config-ctx"
|
||||
import configMerger from "@nuxtjs/tailwindcss/merger";
|
||||
|
||||
|
||||
4
frontend/admin/.nuxt/types/imports.d.ts
vendored
4
frontend/admin/.nuxt/types/imports.d.ts
vendored
@@ -119,7 +119,7 @@ declare global {
|
||||
const useFetch: typeof import('../../node_modules/nuxt/dist/app/composables/fetch').useFetch
|
||||
const useHead: typeof import('../../node_modules/nuxt/dist/app/composables/head').useHead
|
||||
const useHeadSafe: typeof import('../../node_modules/nuxt/dist/app/composables/head').useHeadSafe
|
||||
const useHealthMonitor: typeof import('../../composables/useHealthMonitor').default
|
||||
const useHealthMonitor: typeof import('../../composables/useHealthMonitor').useHealthMonitor
|
||||
const useHydration: typeof import('../../node_modules/nuxt/dist/app/composables/hydrate').useHydration
|
||||
const useI18n: typeof import('../../node_modules/vue-i18n/dist/vue-i18n').useI18n
|
||||
const useId: typeof import('vue').useId
|
||||
@@ -357,7 +357,7 @@ declare module 'vue' {
|
||||
readonly useFetch: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/fetch')['useFetch']>
|
||||
readonly useHead: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useHead']>
|
||||
readonly useHeadSafe: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useHeadSafe']>
|
||||
readonly useHealthMonitor: UnwrapRef<typeof import('../../composables/useHealthMonitor')['default']>
|
||||
readonly useHealthMonitor: UnwrapRef<typeof import('../../composables/useHealthMonitor')['useHealthMonitor']>
|
||||
readonly useHydration: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/hydrate')['useHydration']>
|
||||
readonly useI18n: UnwrapRef<typeof import('../../node_modules/vue-i18n/dist/vue-i18n')['useI18n']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
// Types
|
||||
@@ -32,7 +32,7 @@ export interface HealthMonitorState {
|
||||
lastUpdated: Date | null
|
||||
}
|
||||
|
||||
// Mock data for development/testing
|
||||
// Mock data for development/testing (only for alerts since no backend endpoint yet)
|
||||
const generateMockMetrics = (): HealthMetrics => {
|
||||
return {
|
||||
total_assets: Math.floor(Math.random() * 10000) + 5000,
|
||||
@@ -97,7 +97,17 @@ class HealthMonitorApiService {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('Health monitor API error:', response.status, response.statusText, errorText)
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`)
|
||||
|
||||
// Specific error handling
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication required. Please log in again.')
|
||||
} else if (response.status === 403) {
|
||||
throw new Error('Access forbidden. Admin privileges required.')
|
||||
} else if (response.status === 500) {
|
||||
throw new Error('Server error. Please try again later.')
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
@@ -121,7 +131,7 @@ class HealthMonitorApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Get system alerts
|
||||
// Get system alerts (mocked for now - no backend endpoint)
|
||||
async getSystemAlerts(options?: {
|
||||
severity?: SystemAlert['severity']
|
||||
resolved?: boolean
|
||||
@@ -185,6 +195,7 @@ export const useHealthMonitor = () => {
|
||||
})
|
||||
|
||||
const apiService = new HealthMonitorApiService()
|
||||
let refreshInterval: NodeJS.Timeout | null = null
|
||||
|
||||
// Computed properties
|
||||
const systemStatusColor = computed(() => {
|
||||
@@ -192,7 +203,7 @@ export const useHealthMonitor = () => {
|
||||
|
||||
switch (state.value.metrics.system_status) {
|
||||
case 'healthy': return 'green'
|
||||
case 'degraded': return 'orange'
|
||||
case 'degraded': return 'dark-blue' // Changed from orange to dark-blue for better contrast
|
||||
case 'critical': return 'red'
|
||||
default: return 'grey'
|
||||
}
|
||||
@@ -239,9 +250,7 @@ export const useHealthMonitor = () => {
|
||||
} catch (error) {
|
||||
state.value.error = error instanceof Error ? error.message : 'Failed to fetch health metrics'
|
||||
console.error('Error fetching health metrics:', error)
|
||||
|
||||
// Fallback to mock data
|
||||
state.value.metrics = generateMockMetrics()
|
||||
// NO FALLBACK TO MOCK DATA - let error propagate
|
||||
} finally {
|
||||
state.value.loading = false
|
||||
}
|
||||
@@ -262,7 +271,7 @@ export const useHealthMonitor = () => {
|
||||
state.value.error = error instanceof Error ? error.message : 'Failed to fetch system alerts'
|
||||
console.error('Error fetching system alerts:', error)
|
||||
|
||||
// Fallback to mock data
|
||||
// Fallback to mock data for alerts (since no real endpoint yet)
|
||||
state.value.alerts = generateMockAlerts(5)
|
||||
} finally {
|
||||
state.value.loading = false
|
||||
@@ -292,11 +301,41 @@ export const useHealthMonitor = () => {
|
||||
state.value.alerts = state.value.alerts.filter(alert => alert.id !== alertId)
|
||||
}
|
||||
|
||||
// Initialize
|
||||
const initialize = () => {
|
||||
refreshAll()
|
||||
// Start automatic refresh (30-second interval)
|
||||
const startPolling = (intervalMs: number = 30000) => {
|
||||
stopPolling() // Clear any existing interval
|
||||
|
||||
refreshInterval = setInterval(() => {
|
||||
console.log('Auto-refreshing health monitor data...')
|
||||
refreshAll()
|
||||
}, intervalMs)
|
||||
|
||||
console.log(`Health monitor polling started with ${intervalMs}ms interval`)
|
||||
}
|
||||
|
||||
// Stop automatic refresh
|
||||
const stopPolling = () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval)
|
||||
refreshInterval = null
|
||||
console.log('Health monitor polling stopped')
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize with polling
|
||||
const initialize = (enablePolling: boolean = true) => {
|
||||
refreshAll()
|
||||
|
||||
if (enablePolling) {
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
state: computed(() => state.value),
|
||||
@@ -321,12 +360,14 @@ export const useHealthMonitor = () => {
|
||||
markAlertAsResolved,
|
||||
dismissAlert,
|
||||
initialize,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
|
||||
// Helper functions
|
||||
getAlertColor: (severity: SystemAlert['severity']) => {
|
||||
switch (severity) {
|
||||
case 'info': return 'blue'
|
||||
case 'warning': return 'orange'
|
||||
case 'warning': return 'dark-blue' // Changed from orange to dark-blue
|
||||
case 'critical': return 'red'
|
||||
default: return 'grey'
|
||||
}
|
||||
@@ -346,6 +387,4 @@ export const useHealthMonitor = () => {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default useHealthMonitor
|
||||
}
|
||||
@@ -100,10 +100,10 @@ const closeLegalModal = () => {
|
||||
</main>
|
||||
|
||||
<!-- Daily Quiz Modal -->
|
||||
<DailyQuizModal v-if="authStore.isLoggedIn && route.path !== '/login' && route.path !== '/register'" />
|
||||
<DailyQuizModal v-if="authStore.isLoggedIn && route.path !== '/login' && route.path !== '/register' && route.path !== '/debug'" />
|
||||
|
||||
<!-- Quick Actions FAB -->
|
||||
<QuickActionsFAB v-if="authStore.isLoggedIn && route.path !== '/login' && route.path !== '/register'" />
|
||||
<QuickActionsFAB v-if="authStore.isLoggedIn && route.path !== '/login' && route.path !== '/register' && route.path !== '/debug'" />
|
||||
|
||||
<!-- Legal Modal -->
|
||||
<div v-if="showLegalModal" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm transition-all duration-300">
|
||||
|
||||
@@ -17,6 +17,7 @@ const date = ref(new Date().toISOString().split('T')[0]) // today
|
||||
const description = ref('')
|
||||
const mileage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const showDraftLimitModal = ref(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedAssetId.value) {
|
||||
@@ -56,7 +57,12 @@ const handleSubmit = async () => {
|
||||
emit('close')
|
||||
} catch (error) {
|
||||
console.error('Error saving expense:', error)
|
||||
alert(`Hiba történt a mentés során: ${expenseStore.error || error.message}`)
|
||||
if (error.message === 'DRAFT_LIMIT_REACHED') {
|
||||
// Show custom modal for draft limit
|
||||
showDraftLimitModal.value = true
|
||||
} else {
|
||||
alert(`Hiba történt a mentés során: ${expenseStore.error || error.message}`)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
@@ -65,6 +71,17 @@ const handleSubmit = async () => {
|
||||
const closeModal = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const closeDraftLimitModal = () => {
|
||||
showDraftLimitModal.value = false
|
||||
}
|
||||
|
||||
const openEditVehicle = () => {
|
||||
// TODO: Implement opening edit vehicle modal
|
||||
// For now, just close the draft limit modal and maybe show a message
|
||||
alert('Edit vehicle functionality to be implemented')
|
||||
closeDraftLimitModal()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -188,6 +205,39 @@ const closeModal = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Draft Limit Modal -->
|
||||
<div v-if="showDraftLimitModal" class="fixed inset-0 z-[200] flex items-center justify-center bg-black bg-opacity-70 p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="text-center mb-6">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-100 text-yellow-600 mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-2">You have reached the limit for unregistered vehicles! 🔒</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
Please edit this vehicle and provide the VIN (Chassis Number) or exact Catalog ID to unlock full expense tracking.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="closeDraftLimitModal"
|
||||
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="openEditVehicle"
|
||||
class="flex-1 px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition"
|
||||
>
|
||||
Edit Vehicle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,18 +1,231 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useGarageStore } from '../../stores/garageStore'
|
||||
import { useAuthStore } from '../../stores/authStore'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const garageStore = useGarageStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Form fields
|
||||
const make = ref('')
|
||||
const model = ref('')
|
||||
const generation = ref('')
|
||||
const engine = ref('')
|
||||
const engineId = ref(null) // NEW: Store the engine/catalog ID
|
||||
const licensePlate = ref('')
|
||||
const year = ref('')
|
||||
const fuelType = ref('petrol')
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Catalog data
|
||||
const makes = ref([])
|
||||
const models = ref([])
|
||||
const generations = ref([])
|
||||
const engines = ref([])
|
||||
|
||||
// Loading states
|
||||
const loadingMakes = ref(false)
|
||||
const loadingModels = ref(false)
|
||||
const loadingGenerations = ref(false)
|
||||
const loadingEngines = ref(false)
|
||||
|
||||
// Search filters
|
||||
const makeSearch = ref('')
|
||||
const modelSearch = ref('')
|
||||
const generationSearch = ref('')
|
||||
const engineSearch = ref('')
|
||||
|
||||
// Filtered lists based on search
|
||||
const filteredMakes = ref([])
|
||||
const filteredModels = ref([])
|
||||
const filteredGenerations = ref([])
|
||||
const filteredEngines = ref([])
|
||||
|
||||
// Fetch makes on component mount
|
||||
onMounted(async () => {
|
||||
await fetchMakes()
|
||||
})
|
||||
|
||||
// Watch for search changes
|
||||
watch(makeSearch, (val) => {
|
||||
filteredMakes.value = makes.value.filter(m =>
|
||||
m.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
watch(modelSearch, (val) => {
|
||||
filteredModels.value = models.value.filter(m =>
|
||||
m.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
watch(generationSearch, (val) => {
|
||||
filteredGenerations.value = generations.value.filter(g =>
|
||||
g.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
watch(engineSearch, (val) => {
|
||||
filteredEngines.value = engines.value.filter(e =>
|
||||
e.variant.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// Watch for make selection to fetch models
|
||||
watch(make, async (newMake) => {
|
||||
if (newMake) {
|
||||
await fetchModels(newMake)
|
||||
model.value = ''
|
||||
generation.value = ''
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
} else {
|
||||
models.value = []
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for model selection to fetch generations
|
||||
watch(model, async (newModel) => {
|
||||
if (newModel && make.value) {
|
||||
await fetchGenerations(make.value, newModel)
|
||||
generation.value = ''
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
engines.value = []
|
||||
} else {
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for generation selection to fetch engines
|
||||
watch(generation, async (newGen) => {
|
||||
if (newGen && make.value && model.value) {
|
||||
await fetchEngines(make.value, model.value, newGen)
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
} else {
|
||||
engines.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for engine selection to auto-fill fuel type and capture ID
|
||||
watch(engine, (newEngine) => {
|
||||
if (newEngine) {
|
||||
const selectedEngine = engines.value.find(e => e.variant === newEngine)
|
||||
if (selectedEngine) {
|
||||
// Store the engine ID for API call
|
||||
engineId.value = selectedEngine.id
|
||||
// Auto-fill fuel type if available
|
||||
if (selectedEngine.fuel_type) {
|
||||
fuelType.value = selectedEngine.fuel_type.toLowerCase()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
engineId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// API calls
|
||||
async function fetchMakes() {
|
||||
loadingMakes.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/makes`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch makes: ${response.status}`)
|
||||
makes.value = await response.json()
|
||||
filteredMakes.value = makes.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching makes:', err)
|
||||
error.value = 'Could not load makes. Please try again.'
|
||||
} finally {
|
||||
loadingMakes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchModels(makeName) {
|
||||
loadingModels.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/models?make=${encodeURIComponent(makeName)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch models: ${response.status}`)
|
||||
models.value = await response.json()
|
||||
filteredModels.value = models.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching models:', err)
|
||||
error.value = 'Could not load models for this make.'
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGenerations(makeName, modelName) {
|
||||
loadingGenerations.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/generations?make=${encodeURIComponent(makeName)}&model=${encodeURIComponent(modelName)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch generations: ${response.status}`)
|
||||
generations.value = await response.json()
|
||||
filteredGenerations.value = generations.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching generations:', err)
|
||||
error.value = 'Could not load generations for this model.'
|
||||
} finally {
|
||||
loadingGenerations.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEngines(makeName, modelName, genName) {
|
||||
loadingEngines.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/engines?make=${encodeURIComponent(makeName)}&model=${encodeURIComponent(modelName)}&gen=${encodeURIComponent(genName)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch engines: ${response.status}`)
|
||||
const engineData = await response.json()
|
||||
engines.value = engineData.map(e => ({
|
||||
variant: e.variant,
|
||||
fuel_type: e.fuel_type,
|
||||
id: e.id
|
||||
}))
|
||||
filteredEngines.value = engines.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching engines:', err)
|
||||
error.value = 'Could not load engines for this generation.'
|
||||
} finally {
|
||||
loadingEngines.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
error.value = null
|
||||
isLoading.value = true
|
||||
@@ -23,8 +236,14 @@ const handleSubmit = async () => {
|
||||
make: make.value,
|
||||
model: model.value,
|
||||
licensePlate: licensePlate.value,
|
||||
year: parseInt(year.value),
|
||||
fuelType: fuelType.value
|
||||
year: parseInt(year.value) || new Date().getFullYear(),
|
||||
fuelType: fuelType.value,
|
||||
generation: generation.value,
|
||||
engine: engine.value,
|
||||
// Include the catalog ID (engine ID) for API
|
||||
catalogId: engineId.value,
|
||||
// Include organization ID (use active organization ID)
|
||||
organizationId: authStore.activeOrgId
|
||||
}
|
||||
|
||||
// Call the garage store to add vehicle
|
||||
@@ -36,9 +255,16 @@ const handleSubmit = async () => {
|
||||
// Reset form
|
||||
make.value = ''
|
||||
model.value = ''
|
||||
generation.value = ''
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
licensePlate.value = ''
|
||||
year.value = ''
|
||||
fuelType.value = 'petrol'
|
||||
makes.value = []
|
||||
models.value = []
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
|
||||
// Close modal
|
||||
emit('close')
|
||||
|
||||
@@ -14,18 +14,19 @@ const themeClasses = themeStore.themeClasses
|
||||
const statusColors = {
|
||||
'OK': 'bg-green-100 text-green-800',
|
||||
'Service Due': 'bg-blue-100 text-blue-900',
|
||||
'Warning': 'bg-orange-100 text-orange-800'
|
||||
'Warning': 'bg-orange-100 text-orange-800',
|
||||
'draft': 'bg-yellow-100 text-yellow-800'
|
||||
}
|
||||
|
||||
const brandLogoUrl = (make) => {
|
||||
const cleanMake = make.toLowerCase().replace(/\s+/g, '')
|
||||
const cleanMake = (make || '').toLowerCase().replace(/\s+/g, '')
|
||||
// Use simpleicons CDN
|
||||
return `https://cdn.simpleicons.org/${cleanMake}`
|
||||
}
|
||||
|
||||
// Country flag mapping
|
||||
const getCountryFlag = (make) => {
|
||||
const makeLower = make.toLowerCase()
|
||||
const makeLower = (make || '').toLowerCase()
|
||||
if (makeLower.includes('bmw') || makeLower.includes('mercedes') || makeLower.includes('audi') || makeLower.includes('volkswagen') || makeLower.includes('porsche')) {
|
||||
return 'https://flagcdn.com/w40/de.png'
|
||||
} else if (makeLower.includes('tesla') || makeLower.includes('ford') || makeLower.includes('chevrolet') || makeLower.includes('dodge')) {
|
||||
@@ -98,14 +99,28 @@ const getCountryFlag = (make) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Draft Status & Profile Completion -->
|
||||
<div v-if="vehicle.status === 'draft'" class="mb-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">DRAFT</span>
|
||||
<div class="flex-1">
|
||||
<div class="text-xs text-gray-600 mb-1">Profile: {{ vehicle.profile_completion_percentage || 0 }}% Complete</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-yellow-500 h-2 rounded-full" :style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">Edit vehicle to provide VIN or Catalog ID</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900">{{ (vehicle.mileage / 1000).toFixed(1) }}k</div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ ((vehicle.mileage || 0) / 1000).toFixed(1) }}k</div>
|
||||
<div class="text-sm text-gray-600">km</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900">{{ vehicle.fuelType.charAt(0) }}</div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ vehicle.fuelType?.charAt(0) || 'U' }}</div>
|
||||
<div class="text-sm text-gray-600">Fuel</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useGarageStore } from '@/stores/garageStore'
|
||||
import VehicleCard from './VehicleCard.vue'
|
||||
import FleetTable from './FleetTable.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const garageStore = useGarageStore()
|
||||
|
||||
@@ -138,7 +141,10 @@ const formatCurrency = (amount) => {
|
||||
<div class="text-6xl text-gray-300 mb-4">🚗</div>
|
||||
<h3 class="text-xl font-bold text-gray-500 mb-2">No vehicles yet</h3>
|
||||
<p class="text-gray-600 mb-6">Add your first vehicle to get started</p>
|
||||
<button class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-all duration-300 hover:scale-105 active:scale-95">
|
||||
<button
|
||||
@click="router.push('/vehicles/add')"
|
||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-all duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
Add First Vehicle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import ResetPassword from '../views/ResetPassword.vue';
|
||||
import AddVehicle from '../views/AddVehicle.vue';
|
||||
import AdminStats from '../views/admin/AdminStats.vue';
|
||||
import ProfileSelect from '../views/ProfileSelect.vue';
|
||||
import Debug from '../views/Debug.vue';
|
||||
|
||||
const routes = [
|
||||
// Védett útvonalak
|
||||
@@ -35,6 +36,9 @@ const routes = [
|
||||
{ path: '/register', name: 'Register', component: Register },
|
||||
{ path: '/forgot-password', name: 'ForgotPassword', component: ForgotPassword },
|
||||
{ path: '/reset-password', name: 'ResetPassword', component: ResetPassword },
|
||||
|
||||
// DEBUG útvonal (nyilvános, nincs auth check)
|
||||
{ path: '/debug', name: 'Debug', component: Debug },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -122,4 +122,24 @@ api.interceptors.response.use(
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
export default api
|
||||
|
||||
// Catalog API functions
|
||||
export const catalogApi = {
|
||||
async getMakes() {
|
||||
const response = await api.get('/catalog/makes')
|
||||
return response.data
|
||||
},
|
||||
async getModels(make) {
|
||||
const response = await api.get('/catalog/models', { params: { make } })
|
||||
return response.data
|
||||
},
|
||||
async getGenerations(make, model) {
|
||||
const response = await api.get('/catalog/generations', { params: { make, model } })
|
||||
return response.data
|
||||
},
|
||||
async getEngines(make, model, gen) {
|
||||
const response = await api.get('/catalog/engines', { params: { make, model, gen } })
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export const useAnalyticsStore = defineStore('analytics', () => {
|
||||
}
|
||||
|
||||
// Call real backend API
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/analytics/dashboard`, {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/analytics/dashboard`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
@@ -143,7 +143,7 @@ export const useAnalyticsStore = defineStore('analytics', () => {
|
||||
}
|
||||
|
||||
// Call vehicle summary endpoint
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/analytics/${vehicleId}/summary`, {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/analytics/${vehicleId}/summary`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
||||
@@ -61,7 +61,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await api.get('/api/v1/users/me')
|
||||
const response = await api.get('/users/me')
|
||||
const user = response.data
|
||||
if (user.ui_mode && ['personal', 'fleet'].includes(user.ui_mode)) {
|
||||
mode.value = user.ui_mode
|
||||
@@ -77,7 +77,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
// Save mode to backend via PATCH /users/me/preferences
|
||||
async function saveModeToBackend(newMode) {
|
||||
try {
|
||||
await api.patch('/api/v1/users/me/preferences', {
|
||||
await api.patch('/users/me/preferences', {
|
||||
ui_mode: newMode
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const userEmail = ref(localStorage.getItem('user_email') || '')
|
||||
const userRole = ref(localStorage.getItem('user_role') || '')
|
||||
const userProfile = ref(null) // Full user profile from /users/me
|
||||
const activeOrgId = ref(localStorage.getItem('active_org_id') || null) // Active organization ID
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
@@ -32,7 +33,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
// Call real backend API
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/api/v1/auth/login`, {
|
||||
const response = await fetch(`${apiBaseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
@@ -116,6 +117,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
localStorage.removeItem('user_email')
|
||||
localStorage.removeItem('user_role')
|
||||
localStorage.removeItem('ui_mode') // Also clear UI mode on logout
|
||||
localStorage.removeItem('active_org_id') // Clear active organization ID
|
||||
|
||||
// Reset store state
|
||||
token.value = ''
|
||||
@@ -123,6 +125,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
userEmail.value = ''
|
||||
userRole.value = ''
|
||||
userProfile.value = null
|
||||
activeOrgId.value = null
|
||||
|
||||
// Redirect to login
|
||||
router.push('/login')
|
||||
@@ -135,7 +138,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
try {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/api/v1/users/me`, {
|
||||
const response = await fetch(`${apiBaseUrl}/users/me`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
@@ -166,6 +169,18 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
localStorage.setItem('is_admin', isAdmin.value.toString())
|
||||
}
|
||||
|
||||
// Store active organization ID if available
|
||||
if (data.active_organization_id !== undefined) {
|
||||
activeOrgId.value = data.active_organization_id
|
||||
localStorage.setItem('active_org_id', data.active_organization_id)
|
||||
console.log('AuthStore: Set active organization ID:', data.active_organization_id)
|
||||
} else if (data.scope_id) {
|
||||
// Fallback to scope_id if active_organization_id is not provided
|
||||
activeOrgId.value = data.scope_id
|
||||
localStorage.setItem('active_org_id', data.scope_id)
|
||||
console.log('AuthStore: Set active organization ID from scope_id:', data.scope_id)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AuthStore: Error fetching user profile', err)
|
||||
|
||||
@@ -10,10 +10,16 @@ export const useExpenseStore = defineStore('expense', () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.post('/api/v1/expenses/', expenseData)
|
||||
const response = await api.post('/expenses/', expenseData)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.detail || err.message
|
||||
// Check for DRAFT_LIMIT_REACHED error
|
||||
if (err.response?.status === 403 && err.response?.data?.detail === "DRAFT_LIMIT_REACHED") {
|
||||
const draftError = new Error('DRAFT_LIMIT_REACHED')
|
||||
draftError.response = err.response
|
||||
throw draftError
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
|
||||
@@ -50,7 +50,7 @@ export const useGamificationStore = defineStore('gamification', () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/achievements')
|
||||
const data = await apiFetch('/gamification/achievements')
|
||||
achievements.value = data.achievements || []
|
||||
return data
|
||||
} catch (err) {
|
||||
@@ -68,7 +68,7 @@ export const useGamificationStore = defineStore('gamification', () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/my-badges')
|
||||
const data = await apiFetch('/gamification/my-badges')
|
||||
badges.value = data.map(badge => ({
|
||||
id: badge.badge_id,
|
||||
title: badge.badge_name,
|
||||
@@ -94,7 +94,7 @@ export const useGamificationStore = defineStore('gamification', () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/me')
|
||||
const data = await apiFetch('/gamification/me')
|
||||
userStats.value = data
|
||||
return data
|
||||
} catch (err) {
|
||||
|
||||
@@ -35,10 +35,10 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
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
|
||||
organization_id: vehicle.organizationId || authStore.activeOrgId // Use active org ID, must be present
|
||||
}
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/assets/vehicles`, {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/assets/vehicles`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
@@ -93,12 +93,18 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
const token = authStore.token
|
||||
|
||||
if (!token) {
|
||||
console.error('GarageStore: No authentication token available')
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
console.log('GarageStore: Starting vehicle fetch with token', token.substring(0, 20) + '...')
|
||||
|
||||
// Call real backend API
|
||||
// First try the assets endpoint for user's vehicles
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/assets/vehicles`, {
|
||||
const apiUrl = `${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/assets/vehicles`
|
||||
console.log('GarageStore: Fetching from primary endpoint:', apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
@@ -106,11 +112,15 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
}
|
||||
})
|
||||
|
||||
console.log('GarageStore: Primary endpoint response status:', response.status, response.statusText)
|
||||
|
||||
if (!response.ok) {
|
||||
// If 404, try alternative endpoint
|
||||
if (response.status === 404) {
|
||||
console.log('GarageStore: Primary endpoint returned 404, trying user assets endpoint')
|
||||
// Try user assets endpoint
|
||||
const userResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/users/me/assets`, {
|
||||
const userApiUrl = `${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/users/me/assets`
|
||||
const userResponse = await fetch(userApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
@@ -118,7 +128,20 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
}
|
||||
})
|
||||
|
||||
console.log('GarageStore: User assets endpoint response status:', userResponse.status, userResponse.statusText)
|
||||
|
||||
if (!userResponse.ok) {
|
||||
let errorBody = ''
|
||||
try {
|
||||
errorBody = await userResponse.text()
|
||||
} catch (e) {
|
||||
errorBody = 'Could not read error response'
|
||||
}
|
||||
console.error('GarageStore: User assets endpoint failed:', {
|
||||
status: userResponse.status,
|
||||
statusText: userResponse.statusText,
|
||||
body: errorBody
|
||||
})
|
||||
throw new Error(`Failed to fetch vehicles: ${userResponse.status} ${userResponse.statusText}`)
|
||||
}
|
||||
|
||||
@@ -126,6 +149,17 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
console.log('GarageStore: Fetched vehicles from user assets endpoint', data)
|
||||
vehicles.value = transformApiResponse(data)
|
||||
} else {
|
||||
let errorBody = ''
|
||||
try {
|
||||
errorBody = await response.text()
|
||||
} catch (e) {
|
||||
errorBody = 'Could not read error response'
|
||||
}
|
||||
console.error('GarageStore: Primary endpoint failed:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorBody
|
||||
})
|
||||
throw new Error(`Failed to fetch vehicles: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
} else {
|
||||
@@ -135,6 +169,7 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('GarageStore: Error fetching vehicles', err)
|
||||
console.error('GarageStore: Full error stack:', err.stack)
|
||||
error.value = err.message
|
||||
// NO MORE MOCK DATA FALLBACK - fail properly
|
||||
vehicles.value = []
|
||||
@@ -142,6 +177,7 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
console.log('GarageStore: Fetch completed, vehicles count:', vehicles.value.length)
|
||||
return vehicles.value
|
||||
}
|
||||
|
||||
@@ -152,23 +188,41 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
data = [data]
|
||||
}
|
||||
|
||||
return data.map(item => ({
|
||||
id: item.id || item.asset_id || 0,
|
||||
make: item.make || item.brand || item.vehicle_make || 'Unknown',
|
||||
model: item.model || item.vehicle_model || 'Unknown',
|
||||
year: item.year || item.year_of_manufacture || item.manufacture_year || 2023,
|
||||
licensePlate: item.license_plate || item.registration_number || 'N/A',
|
||||
status: (item.status === 'active' || item.is_active) ? 'OK' : 'Service Due',
|
||||
monthlyExpense: item.monthly_expense || item.average_monthly_cost || 0,
|
||||
fuelType: item.fuel_type || item.fuel || 'Unknown',
|
||||
mileage: item.mileage || item.current_mileage || item.odometer_reading || 0,
|
||||
imageUrl: item.image_url || item.photo_url || getDefaultImage(item.make || item.brand)
|
||||
}))
|
||||
return data.map(item => {
|
||||
// Safely extract catalog properties with optional chaining
|
||||
const catalog = item.catalog || {}
|
||||
const catalogMake = catalog?.make || catalog?.brand || null
|
||||
const catalogModel = catalog?.model || null
|
||||
const catalogFuelType = catalog?.fuel_type || catalog?.fuel || null
|
||||
const catalogYear = catalog?.year || catalog?.year_of_manufacture || null
|
||||
|
||||
// Safely extract profile completion percentage
|
||||
const profileCompletion = item.profile_completion_percentage ||
|
||||
item.profile_completion ||
|
||||
(item.profile_status === 'complete' ? 100 : 0) || 0
|
||||
|
||||
return {
|
||||
id: item.id || item.asset_id || 0,
|
||||
make: item.make || item.brand || item.vehicle_make || catalogMake || 'Unknown',
|
||||
model: item.model || item.vehicle_model || catalogModel || 'Unknown',
|
||||
year: item.year || item.year_of_manufacture || item.manufacture_year || catalogYear || 2023,
|
||||
licensePlate: item.license_plate || item.registration_number || 'N/A',
|
||||
status: item.status || (item.is_active ? 'active' : 'draft'),
|
||||
profile_completion_percentage: profileCompletion,
|
||||
monthlyExpense: item.monthly_expense || item.average_monthly_cost || 0,
|
||||
fuelType: item.fuel_type || item.fuel || catalogFuelType || 'Unknown',
|
||||
mileage: item.mileage || item.current_mileage || item.odometer_reading || 0,
|
||||
imageUrl: item.image_url || item.photo_url || getDefaultImage(item.make || item.brand || catalogMake),
|
||||
// Include catalog object for template access with null safety
|
||||
catalog: catalog || null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to get default image based on make
|
||||
function getDefaultImage(make) {
|
||||
const makeLower = make.toLowerCase()
|
||||
// Handle null/undefined make with optional chaining and fallback
|
||||
const makeLower = (make || '').toLowerCase()
|
||||
if (makeLower.includes('bmw')) {
|
||||
return 'https://images.unsplash.com/photo-1555215695-3004980ad54e?w=400&h=300&fit=crop'
|
||||
} else if (makeLower.includes('audi')) {
|
||||
|
||||
@@ -62,7 +62,7 @@ export const useQuizStore = defineStore('quiz', () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/quiz/stats')
|
||||
const data = await apiFetch('/gamification/quiz/stats')
|
||||
userPoints.value = data.total_quiz_points || 0
|
||||
currentStreak.value = data.current_streak || 0
|
||||
lastPlayedDate.value = data.last_played || null
|
||||
@@ -89,7 +89,7 @@ export const useQuizStore = defineStore('quiz', () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/api/v1/gamification/quiz/daily')
|
||||
const data = await apiFetch('/gamification/quiz/daily')
|
||||
questions.value = data.questions || []
|
||||
return data
|
||||
} catch (err) {
|
||||
@@ -109,7 +109,7 @@ export const useQuizStore = defineStore('quiz', () => {
|
||||
|
||||
async function answerQuestion(questionId, selectedOptionIndex) {
|
||||
try {
|
||||
const response = await apiFetch('/api/v1/gamification/quiz/answer', {
|
||||
const response = await apiFetch('/gamification/quiz/answer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
question_id: questionId,
|
||||
@@ -136,7 +136,7 @@ export const useQuizStore = defineStore('quiz', () => {
|
||||
|
||||
async function completeDailyQuiz() {
|
||||
try {
|
||||
await apiFetch('/api/v1/gamification/quiz/complete', {
|
||||
await apiFetch('/gamification/quiz/complete', {
|
||||
method: 'POST'
|
||||
})
|
||||
lastPlayedDate.value = new Date().toISOString()
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import api from '@/services/api'
|
||||
|
||||
const form = ref({
|
||||
category: 'REFUELING',
|
||||
@@ -47,7 +47,25 @@ const form = ref({
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await axios.post(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/expenses/add`, form.value)
|
||||
// Map frontend fields to backend schema
|
||||
const categoryMap = {
|
||||
'REFUELING': 'fuel',
|
||||
'SERVICE': 'service',
|
||||
'INSURANCE': 'insurance',
|
||||
'TOLL': 'toll',
|
||||
'FINE': 'fine'
|
||||
}
|
||||
const payload = {
|
||||
cost_type: categoryMap[form.value.category] || form.value.category.toLowerCase(),
|
||||
amount_local: form.value.amount,
|
||||
currency_local: 'HUF',
|
||||
mileage_at_cost: form.value.odometer_value,
|
||||
date: new Date(form.value.date).toISOString(),
|
||||
asset_id: form.value.vehicle_id,
|
||||
description: null,
|
||||
data: {}
|
||||
}
|
||||
await api.post('/expenses/', payload)
|
||||
alert("Sikeresen mentve!")
|
||||
} catch (err) {
|
||||
alert("Hiba történt a mentéskor.")
|
||||
|
||||
@@ -33,10 +33,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import api from '@/services/api'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const searchQuery = ref('')
|
||||
const brands = ref([])
|
||||
const selectedBrand = ref(null)
|
||||
@@ -44,7 +46,7 @@ const form = ref({ model_name: '', plate: '', vin: '', current_odo: 0 })
|
||||
|
||||
const searchBrands = async () => {
|
||||
if (searchQuery.value.length < 2) { brands.value = []; return; }
|
||||
const res = await axios.get(`http://192.168.100.43:8000/api/v1/vehicles/search/brands?q=${searchQuery.value}`)
|
||||
const res = await api.get(`/vehicles/search/brands?q=${searchQuery.value}`)
|
||||
brands.value = res.data.data
|
||||
}
|
||||
|
||||
@@ -54,15 +56,16 @@ const selectBrand = (brand) => {
|
||||
}
|
||||
|
||||
const saveVehicle = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
try {
|
||||
await axios.post('http://192.168.100.43:8000/api/v1/vehicles/register', {
|
||||
brand_id: selectedBrand.value.id,
|
||||
...form.value
|
||||
}, { headers: { Authorization: `Bearer ${token}` }})
|
||||
await api.post('/assets/vehicles', {
|
||||
vin: form.value.vin || null,
|
||||
license_plate: form.value.plate || 'N/A',
|
||||
catalog_id: null, // draft mode, no catalog mapping yet
|
||||
organization_id: authStore.activeOrgId // Use active organization ID, must be present
|
||||
})
|
||||
router.push('/')
|
||||
} catch (err) {
|
||||
alert("Hiba a mentés során.")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -5,17 +5,23 @@ import VehicleShowcase from '@/components/garage/VehicleShowcase.vue'
|
||||
import AchievementShowcase from '@/components/gamification/AchievementShowcase.vue'
|
||||
import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard.vue'
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const report = ref(null)
|
||||
const loading = ref(true)
|
||||
const themeStore = useThemeStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const themeClasses = computed(() => themeStore.themeClasses)
|
||||
|
||||
onMounted(async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
try {
|
||||
const res = await axios.get(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/reports/summary/latest`, {
|
||||
// Fetch user profile
|
||||
await authStore.fetchUserProfile()
|
||||
|
||||
// Fetch report summary
|
||||
const res = await axios.get(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/reports/summary/latest`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
report.value = res.data
|
||||
|
||||
369
frontend/src/views/Debug.vue
Normal file
369
frontend/src/views/Debug.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="debug-page">
|
||||
<h1>🚨 DEBUG VIEW - RAW STATE INSPECTOR</h1>
|
||||
|
||||
<div class="credentials">
|
||||
<h2>Test Credentials</h2>
|
||||
<p><strong>Email:</strong> tester_pro@profibot.hu</p>
|
||||
<p><strong>Password:</strong> Password123!</p>
|
||||
<p class="note">Use these credentials to test the login flow</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<h2>Auth Store Actions</h2>
|
||||
<div class="button-group">
|
||||
<button @click="triggerLogin" :disabled="false">
|
||||
🔐 Trigger Login (tester_pro@profibot.hu)
|
||||
</button>
|
||||
|
||||
<button @click="fetchUserProfile" :disabled="!authStore.isLoggedIn">
|
||||
👤 Fetch User Profile
|
||||
</button>
|
||||
|
||||
<button @click="fetchVehicles" :disabled="!authStore.isLoggedIn">
|
||||
🚗 Fetch Vehicles (Garage)
|
||||
</button>
|
||||
|
||||
<button @click="clearState">
|
||||
🗑️ Clear All State
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<p><strong>Status:</strong> {{ statusMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Auth Store State -->
|
||||
<div class="panel">
|
||||
<h2>🔐 Auth Store State</h2>
|
||||
<div class="state-info">
|
||||
<p><span class="label">isLoggedIn:</span> <span :class="authStore.isLoggedIn ? 'yes' : 'no'">{{ authStore.isLoggedIn ? 'YES' : 'NO' }}</span></p>
|
||||
<p><span class="label">isAdmin:</span> <span :class="authStore.isAdmin ? 'yes' : 'no'">{{ authStore.isAdmin ? 'YES' : 'NO' }}</span></p>
|
||||
<p><span class="label">isTester:</span> <span :class="authStore.isTester ? 'yes' : 'no'">{{ authStore.isTester ? 'YES' : 'NO' }}</span></p>
|
||||
</div>
|
||||
|
||||
<h3>Raw JSON State:</h3>
|
||||
<pre>{{ authStoreJSON }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Garage Store State -->
|
||||
<div class="panel">
|
||||
<h2>🚗 Garage Store State</h2>
|
||||
<div class="state-info">
|
||||
<p><span class="label">Vehicles Count:</span> {{ garageStore.vehicles ? garageStore.vehicles.length : 0 }}</p>
|
||||
<p><span class="label">Loading:</span> {{ garageStore.loading ? 'YES' : 'NO' }}</p>
|
||||
</div>
|
||||
|
||||
<h3>Raw JSON State:</h3>
|
||||
<pre>{{ garageStoreJSON }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LocalStorage Inspection -->
|
||||
<div class="panel">
|
||||
<h2>💾 LocalStorage Inspection</h2>
|
||||
<div class="storage-grid">
|
||||
<div class="storage-item">
|
||||
<div class="storage-label">token</div>
|
||||
<div class="storage-value">{{ localStorage.token ? `${localStorage.token.substring(0, 30)}...` : '(empty)' }}</div>
|
||||
</div>
|
||||
<div class="storage-item">
|
||||
<div class="storage-label">user_email</div>
|
||||
<div class="storage-value">{{ localStorage.user_email || '(empty)' }}</div>
|
||||
</div>
|
||||
<div class="storage-item">
|
||||
<div class="storage-label">is_admin</div>
|
||||
<div class="storage-value">{{ localStorage.is_admin || '(empty)' }}</div>
|
||||
</div>
|
||||
<div class="storage-item">
|
||||
<div class="storage-label">user_role</div>
|
||||
<div class="storage-value">{{ localStorage.user_role || '(empty)' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="refreshLocalStorage">
|
||||
🔄 Refresh LocalStorage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useGarageStore } from '@/stores/garageStore'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const garageStore = useGarageStore()
|
||||
|
||||
const statusMessage = ref('Ready')
|
||||
const localStorage = ref({})
|
||||
|
||||
// Computed properties for JSON display
|
||||
const authStoreJSON = computed(() => {
|
||||
return JSON.stringify({
|
||||
token: authStore.token,
|
||||
isLoggedIn: authStore.isLoggedIn,
|
||||
isAdmin: authStore.isAdmin,
|
||||
isTester: authStore.isTester,
|
||||
userEmail: authStore.userEmail,
|
||||
userRole: authStore.userRole,
|
||||
userProfile: authStore.userProfile,
|
||||
displayName: authStore.displayName,
|
||||
activeOrgId: authStore.activeOrgId
|
||||
}, null, 2)
|
||||
})
|
||||
|
||||
const garageStoreJSON = computed(() => {
|
||||
return JSON.stringify({
|
||||
vehicles: garageStore.vehicles,
|
||||
loading: garageStore.loading,
|
||||
error: garageStore.error,
|
||||
selectedVehicle: garageStore.selectedVehicle
|
||||
}, null, 2)
|
||||
})
|
||||
|
||||
// Functions
|
||||
const triggerLogin = async () => {
|
||||
statusMessage.value = 'Logging in with tester_pro@profibot.hu...'
|
||||
try {
|
||||
await authStore.login('tester_pro@profibot.hu', 'Password123!')
|
||||
statusMessage.value = 'Login successful! Check auth store state.'
|
||||
refreshLocalStorage()
|
||||
} catch (error) {
|
||||
statusMessage.value = `Login failed: ${error.message}`
|
||||
console.error('Login error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
statusMessage.value = 'Fetching user profile...'
|
||||
try {
|
||||
await authStore.fetchUserProfile()
|
||||
statusMessage.value = 'User profile fetched successfully!'
|
||||
} catch (error) {
|
||||
statusMessage.value = `Failed to fetch user profile: ${error.message}`
|
||||
console.error('Fetch user profile error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchVehicles = async () => {
|
||||
statusMessage.value = 'Fetching vehicles...'
|
||||
try {
|
||||
await garageStore.fetchVehicles()
|
||||
statusMessage.value = `Vehicles fetched successfully! Found ${garageStore.vehicles?.length || 0} vehicles.`
|
||||
} catch (error) {
|
||||
statusMessage.value = `Failed to fetch vehicles: ${error.message}`
|
||||
console.error('Fetch vehicles error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const clearState = () => {
|
||||
authStore.logout()
|
||||
garageStore.$reset()
|
||||
refreshLocalStorage()
|
||||
statusMessage.value = 'All state cleared!'
|
||||
}
|
||||
|
||||
const refreshLocalStorage = () => {
|
||||
localStorage.value = {
|
||||
token: window.localStorage.getItem('token') || '',
|
||||
user_email: window.localStorage.getItem('user_email') || '',
|
||||
is_admin: window.localStorage.getItem('is_admin') || '',
|
||||
user_role: window.localStorage.getItem('user_role') || '',
|
||||
refresh_token: window.localStorage.getItem('refresh_token') ? '***' : '',
|
||||
ui_mode: window.localStorage.getItem('ui_mode') || '',
|
||||
active_org_id: window.localStorage.getItem('active_org_id') || ''
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
refreshLocalStorage()
|
||||
statusMessage.value = 'Debug view loaded. Use buttons to test auth flow.'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.debug-page {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
font-family: monospace;
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #1e3a8a; /* sötétkék */
|
||||
border-bottom: 2px solid #1e3a8a;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 15px 0 10px 0;
|
||||
color: #1e3a8a; /* sötétkék */
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0 5px 0;
|
||||
color: #1e3a8a; /* sötétkék */
|
||||
}
|
||||
|
||||
.credentials {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.credentials h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
background: #1e3a8a; /* sötétkék */
|
||||
color: white;
|
||||
border: 1px solid #1e3a8a;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #1e40af; /* sötétkék - sötétebb */
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
color: #666;
|
||||
border-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
margin-top: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status p {
|
||||
margin: 0;
|
||||
color: #1e3a8a; /* sötétkék */
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.state-info {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.state-info p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #1e3a8a; /* sötétkék */
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.yes {
|
||||
color: #065f46; /* zöld */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.no {
|
||||
color: #991b1b; /* piros */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #1e1e1e;
|
||||
color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.storage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.storage-item {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.storage-label {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: #1e3a8a; /* sötétkék */
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.storage-value {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +1,141 @@
|
||||
<template>
|
||||
<div class="max-w-md mx-auto mt-10 p-8 bg-white rounded-2xl shadow-xl">
|
||||
<h2 class="text-2xl font-bold mb-6 text-center">Új Széf létrehozása</h2>
|
||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||
<input v-model="form.first_name" type="text" placeholder="Keresztnév" required class="w-full p-3 border rounded-lg" />
|
||||
<input v-model="form.last_name" type="text" placeholder="Vezetéknév" required class="w-full p-3 border rounded-lg" />
|
||||
<input v-model="form.email" type="email" placeholder="E-mail" required class="w-full p-3 border rounded-lg" />
|
||||
<input v-model="form.password" type="password" placeholder="Jelszó" required class="w-full p-3 border rounded-lg" />
|
||||
<button class="w-full bg-green-600 text-white py-3 rounded-lg font-bold hover:bg-green-700 transition">Fiók létrehozása</button>
|
||||
</form>
|
||||
<p v-if="msg" class="mt-4 text-center font-medium text-green-600">{{ msg }}</p>
|
||||
|
||||
<!-- Registration Form (shown when not successful) -->
|
||||
<div v-if="!success">
|
||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||
<input v-model="form.first_name" type="text" placeholder="Keresztnév" required class="w-full p-3 border rounded-lg" />
|
||||
<input v-model="form.last_name" type="text" placeholder="Vezetéknév" required class="w-full p-3 border rounded-lg" />
|
||||
<input v-model="form.email" type="email" placeholder="E-mail" required class="w-full p-3 border rounded-lg" />
|
||||
<input v-model="form.password" type="password" placeholder="Jelszó" required class="w-full p-3 border rounded-lg" />
|
||||
<button
|
||||
:disabled="loading"
|
||||
class="w-full bg-green-600 text-white py-3 rounded-lg font-bold hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="loading">Feldolgozás...</span>
|
||||
<span v-else>Fiók létrehozása</span>
|
||||
</button>
|
||||
</form>
|
||||
<p v-if="msg && !success" :class="['mt-4 text-center font-medium', isError ? 'text-red-600' : 'text-green-600']">{{ msg }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Card (shown after successful registration) -->
|
||||
<div v-else class="text-center">
|
||||
<div class="mb-6">
|
||||
<!-- Animated checkmark -->
|
||||
<div class="relative inline-flex items-center justify-center w-24 h-24">
|
||||
<div class="absolute inset-0 bg-green-100 rounded-full"></div>
|
||||
<svg class="relative w-16 h-16 text-green-600 animate-checkmark" viewBox="0 0 52 52">
|
||||
<circle class="stroke-current" cx="26" cy="26" r="25" fill="none" stroke-width="2"/>
|
||||
<path class="stroke-current" fill="none" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-3">Sikeres regisztráció!</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
Elküldtünk egy megerősítő e-mailt a(z) <span class="font-semibold">{{ form.email }}</span> címre.
|
||||
Kérjük, ellenőrizd a postaládádat és kattints a linkre a fiók aktiválásához.
|
||||
</p>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6 text-left">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>Fontos:</strong> A megerősítő e-mail néhány perc múlva érkezhet meg.
|
||||
Ha nem találod, ellenőrizd a spam mappát is.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="resetForm"
|
||||
class="w-full bg-gray-200 text-gray-800 py-3 rounded-lg font-bold hover:bg-gray-300 transition"
|
||||
>
|
||||
Új regisztráció
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import api from '@/services/api'
|
||||
|
||||
const form = ref({ email: '', password: '', first_name: '', last_name: '' })
|
||||
const msg = ref('')
|
||||
const isError = ref(false)
|
||||
const loading = ref(false)
|
||||
const success = ref(false)
|
||||
|
||||
const handleRegister = async () => {
|
||||
msg.value = ""; // Üzenet alaphelyzetbe
|
||||
isError.value = false
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await axios.post(`http://192.168.100.43:8000/api/v2/auth/register`, null, { params: form.value });
|
||||
msg.value = "Sikeres regisztráció! Kérlek aktiváld az e-mailedet.";
|
||||
const res = await api.post('/auth/register', form.value);
|
||||
// Sikeres válasz: 201 Created
|
||||
success.value = true
|
||||
msg.value = `Verification email sent to ${form.value.email}!`;
|
||||
isError.value = false
|
||||
} catch (err) {
|
||||
isError.value = true
|
||||
success.value = false
|
||||
// ITT A JAVÍTÁS: kiolvassuk a backend hibaüzenetét
|
||||
if (err.response && err.response.data && err.response.data.detail) {
|
||||
msg.value = "Hiba: " + err.response.data.detail;
|
||||
} else {
|
||||
msg.value = "Hiba történt a regisztráció során.";
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
const resetForm = () => {
|
||||
success.value = false
|
||||
form.value = { email: '', password: '', first_name: '', last_name: '' }
|
||||
msg.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes checkmark {
|
||||
0% {
|
||||
stroke-dashoffset: 100;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-checkmark circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||
}
|
||||
|
||||
.animate-checkmark path {
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user