2026.03.29 20:00 Gitea_manager javítás előtt

This commit is contained in:
Roo
2026-03-29 17:59:06 +00:00
parent 03258db091
commit ba8b6579ef
148 changed files with 7951 additions and 591 deletions

View File

@@ -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';

View File

@@ -1 +1 @@
{"id":"dev","timestamp":1774433357734}
{"id":"dev","timestamp":1774557833950}

View File

@@ -1 +1 @@
{"id":"dev","timestamp":1774433357734,"prerendered":[]}
{"id":"dev","timestamp":1774557833950,"prerendered":[]}

View File

@@ -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"
}
}
}

View File

@@ -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" />

View File

@@ -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";

View File

@@ -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']>

View File

@@ -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
}

View File

@@ -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">

View File

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

View File

@@ -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')

View File

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

View File

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

View File

@@ -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({

View File

@@ -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
}
}

View File

@@ -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}`,

View File

@@ -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) {

View File

@@ -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)

View File

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

View File

@@ -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) {

View File

@@ -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')) {

View File

@@ -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()

View File

@@ -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.")

View File

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

View File

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

View 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>

View File

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