2026.06.04 frontend építés közben

This commit is contained in:
Roo
2026-06-04 07:26:22 +00:00
parent 7adf6cc3e3
commit 59a30ac428
3302 changed files with 24091 additions and 1771 deletions

View File

@@ -1,231 +0,0 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useQuizStore } from '@/stores/quizStore'
const quizStore = useQuizStore()
const showModal = ref(false)
const currentQuestionIndex = ref(0)
const selectedOption = ref(null)
const showResult = ref(false)
const isCorrect = ref(false)
const resultExplanation = ref('')
const isLoading = ref(false)
const currentQuestion = computed(() => quizStore.questions[currentQuestionIndex.value])
const totalQuestions = computed(() => quizStore.totalQuestions)
const progress = computed(() => ((currentQuestionIndex.value + 1) / totalQuestions.value) * 100)
// Auto-show modal after 3-5 seconds if canPlayToday
onMounted(() => {
if (quizStore.canPlayToday) {
setTimeout(() => {
openModal()
}, 3500) // 3.5 seconds
}
})
async function openModal() {
if (!quizStore.canPlayToday) {
alert('Már játszottál ma! Holnap próbáld újra.')
return
}
isLoading.value = true
try {
// Fetch daily quiz questions from API
await quizStore.fetchDailyQuiz()
resetQuiz()
showModal.value = true
} catch (error) {
console.error('Failed to load daily quiz:', error)
alert('Hiba történt a kvíz betöltése közben. Próbáld újra később.')
} finally {
isLoading.value = false
}
}
function closeModal() {
showModal.value = false
}
function resetQuiz() {
currentQuestionIndex.value = 0
selectedOption.value = null
showResult.value = false
isCorrect.value = false
resultExplanation.value = ''
}
async function selectOption(optionIndex) {
if (showResult.value) return
selectedOption.value = optionIndex
try {
const result = await quizStore.answerQuestion(currentQuestion.value.id, optionIndex)
isCorrect.value = result.is_correct
resultExplanation.value = result.explanation
showResult.value = true
} catch (error) {
console.error('Failed to submit answer:', error)
alert('Hiba történt a válasz beküldése közben.')
}
}
function nextQuestion() {
if (currentQuestionIndex.value < totalQuestions.value - 1) {
currentQuestionIndex.value++
selectedOption.value = null
showResult.value = false
} else {
finishQuiz()
}
}
async function finishQuiz() {
try {
await quizStore.completeDailyQuiz()
showModal.value = false
alert(`Kvíz befejezve! Szerezttél ${quizStore.userPoints} pontot. Streak: ${quizStore.currentStreak}`)
} catch (error) {
console.error('Failed to complete quiz:', error)
alert('Hiba történt a kvíz befejezése közben.')
}
}
function skipToday() {
quizStore.completeDailyQuiz() // mark as played today
closeModal()
}
</script>
<template>
<div v-if="showModal" class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 p-4">
<div class="relative w-full max-w-2xl rounded-2xl bg-gradient-to-br from-blue-50 to-white shadow-2xl p-6 md:p-8 border border-blue-200">
<!-- Close button -->
<button @click="closeModal" class="absolute top-4 right-4 text-gray-500 hover:text-gray-800 text-2xl">
&times;
</button>
<!-- Header -->
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 mb-4">
<span class="text-3xl">🧠</span>
</div>
<h2 class="text-3xl font-bold text-gray-900">Napi Kvíz</h2>
<p class="text-gray-600 mt-2">Teszteld tudásod és szerezz pontokat!</p>
<div class="mt-4 flex items-center justify-between text-sm text-gray-700">
<div class="flex items-center gap-2">
<span class="font-bold">Pontok:</span>
<span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full">{{ quizStore.userPoints }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-bold">Streak:</span>
<span class="bg-amber-100 text-amber-800 px-3 py-1 rounded-full">{{ quizStore.currentStreak }} nap</span>
</div>
</div>
</div>
<!-- Progress bar -->
<div class="mb-8">
<div class="flex justify-between text-sm text-gray-700 mb-2">
<span>Kérdés {{ currentQuestionIndex + 1 }} / {{ totalQuestions }}</span>
<span>{{ Math.round(progress) }}%</span>
</div>
<div class="h-3 bg-gray-200 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-500" :style="{ width: `${progress}%` }"></div>
</div>
</div>
<!-- Question -->
<div class="mb-8">
<h3 class="text-xl font-semibold text-gray-900 mb-6">{{ currentQuestion.question }}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
v-for="(option, idx) in currentQuestion.options"
:key="idx"
@click="selectOption(idx)"
class="p-4 text-left rounded-xl border-2 transition-all duration-300"
:class="{
'border-blue-500 bg-blue-50': selectedOption === idx,
'border-gray-300 hover:border-blue-400 hover:bg-blue-50': selectedOption === null && !showResult,
'border-green-500 bg-green-50': showResult && idx === currentQuestion.correctAnswer,
'border-red-300 bg-red-50': showResult && selectedOption === idx && !isCorrect,
}"
:disabled="showResult"
>
<div class="flex items-center">
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center mr-3"
:class="{
'bg-blue-100 text-blue-800': selectedOption === idx && !showResult,
'bg-green-100 text-green-800': showResult && idx === currentQuestion.correctAnswer,
'bg-red-100 text-red-800': showResult && selectedOption === idx && !isCorrect,
'bg-gray-100 text-gray-800': selectedOption !== idx && !showResult,
}">
{{ String.fromCharCode(65 + idx) }}
</div>
<span class="font-medium">{{ option }}</span>
</div>
</button>
</div>
</div>
<!-- Result & Explanation -->
<div v-if="showResult" class="mb-8 p-5 rounded-xl" :class="isCorrect ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-full flex items-center justify-center" :class="isCorrect ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'">
{{ isCorrect ? '✅' : '❌' }}
</div>
<h4 class="text-xl font-bold" :class="isCorrect ? 'text-green-800' : 'text-red-800'">
{{ isCorrect ? 'Helyes válasz!' : 'Sajnos nem talált!' }}
</h4>
</div>
<p class="text-gray-800">{{ resultExplanation }}</p>
<div class="mt-4 text-sm text-gray-700">
<span class="font-bold">Pontok:</span> {{ isCorrect ? '+10' : '0' }} |
<span class="font-bold">Streak:</span> {{ isCorrect ? 'növelve' : 'nullázva' }}
</div>
</div>
<!-- Actions -->
<div class="flex flex-col sm:flex-row gap-4">
<button
v-if="!showResult"
@click="skipToday"
class="flex-1 py-3 px-6 rounded-xl border-2 border-gray-300 text-gray-700 font-semibold hover:bg-gray-100 transition"
>
Emlékeztess később
</button>
<button
v-if="showResult && currentQuestionIndex < totalQuestions - 1"
@click="nextQuestion"
class="flex-1 py-3 px-6 rounded-xl bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold hover:opacity-90 transition"
>
Következő kérdés
</button>
<button
v-if="showResult && currentQuestionIndex === totalQuestions - 1"
@click="finishQuiz"
class="flex-1 py-3 px-6 rounded-xl bg-gradient-to-r from-green-500 to-emerald-600 text-white font-bold hover:opacity-90 transition"
>
Kvíz befejezése
</button>
<button
@click="closeModal"
class="flex-1 py-3 px-6 rounded-xl bg-gray-200 text-gray-800 font-semibold hover:bg-gray-300 transition"
>
Bezárás
</button>
</div>
<!-- Footer note -->
<div class="mt-8 text-center text-sm text-gray-500">
A napi kvíz csak egyszer játszható 24 óránként. Streaked növeléséhez válaszolj helyesen minden nap!
</div>
</div>
</div>
</template>
<style scoped>
/* Additional custom styles if needed */
</style>

View File

@@ -0,0 +1,624 @@
<template>
<Transition name="slide-fade">
<div
v-if="visible"
class="fixed inset-0 z-[100] flex items-center justify-center px-4"
@click.self="$emit('close')"
>
<!-- Dark Blurred Backdrop -->
<div
class="absolute inset-0 bg-[#062535]/80 backdrop-blur-sm"
@click="$emit('close')"
></div>
<!-- 3D Perspective Container -->
<div class="perspective-1000 relative z-10 w-full max-w-md">
<!-- Flippable Card Body -->
<div
class="relative transition-transform duration-700 transform-style-3d"
:class="{
'rotate-y-180': authMode === 'register',
'rotate-x-180': authMode === 'forgot'
}"
>
<!-- ==================== FRONT FACE: LOGIN ==================== -->
<form
@submit.prevent="handleLogin"
class="backface-hidden w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
:class="{ 'pointer-events-none': authMode !== 'login' }"
>
<!-- Close Button (X) -->
<button
type="button"
@click="$emit('close')"
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<!-- Title -->
<h2 class="mb-8 text-center text-2xl font-bold text-white">
Belépés a garázsba
</h2>
<!-- Email Input -->
<div class="mb-5">
<label for="login-email" class="mb-2 block text-sm font-medium text-white/80">
Email
</label>
<input
id="login-email"
v-model="email"
type="email"
placeholder="pelda@email.com"
autocomplete="email"
required
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
/>
</div>
<!-- Password Input -->
<div class="mb-6 relative">
<label for="login-password" class="mb-2 block text-sm font-medium text-white/80">
Jelszó
</label>
<input
id="login-password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="••••••••"
autocomplete="current-password"
required
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 pr-12 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
/>
<!-- Eye icon (hold to show) -->
<button
type="button"
@mousedown="showPassword = true"
@mouseup="showPassword = false"
@mouseleave="showPassword = false"
@touchstart.prevent="showPassword = true"
@touchend="showPassword = false"
class="absolute right-3 top-[42px] text-white/40 hover:text-white/70 transition-colors cursor-pointer"
tabindex="-1"
aria-label="Jelszó megtekintése"
>
<!-- Eye open (password visible) -->
<svg
v-if="showPassword"
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
<path d="M1 1l22 22" />
</svg>
<!-- Eye closed (password hidden) -->
<svg
v-else
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</div>
<!-- Forgot Password Link (right-aligned, always visible) -->
<div class="mb-6 text-right">
<a
href="#"
@click.prevent="switchMode('forgot')"
class="text-xs text-[#75A882] hover:text-[#D4AF37] transition-colors"
>
Elfelejtetted a jelszavad?
</a>
</div>
<!-- Error Message -->
<div
v-if="authStore.error && authMode === 'login'"
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
>
{{ authStore.error }}
</div>
<!-- Login Button (Premium) -->
<button
type="submit"
:disabled="authStore.isLoading"
class="btn-premium w-full px-4 py-3 text-lg"
>
{{ authStore.isLoading ? 'Kérlek várj...' : 'Belépés' }}
</button>
<!-- Divider -->
<div class="my-6 flex items-center gap-3">
<span class="flex-1 border-t border-white/10"></span>
<span class="text-sm text-white/40">vagy</span>
<span class="flex-1 border-t border-white/10"></span>
</div>
<!-- Social Login Placeholders -->
<div class="mb-6 grid grid-cols-2 gap-3">
<button
type="button"
class="flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/50 transition-all duration-200 hover:border-white/20 hover:text-white/80"
>
<!-- Google Logo Placeholder -->
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Google
</button>
<button
type="button"
class="flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/50 transition-all duration-200 hover:border-white/20 hover:text-white/80"
>
<!-- Apple Logo Placeholder -->
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
</svg>
Apple
</button>
</div>
<!-- Registration Link (Triggers Flip) -->
<p class="text-center text-sm text-white/50">
Még nincs garázsod?
<a
href="#"
@click.prevent="switchMode('register')"
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
>
Regisztráció
</a>
</p>
</form>
<!-- ==================== BACK FACE: REGISTER ==================== -->
<form
v-if="activeBackFace === 'register'"
@submit.prevent="handleRegister"
class="backface-hidden rotate-y-180 absolute inset-0 w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
>
<!-- Close Button (X) -->
<button
type="button"
@click="$emit('close')"
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<!-- Title -->
<h2 class="mb-8 text-center text-2xl font-bold text-white">
Új Garázs Nyitása
</h2>
<!-- Last Name Input -->
<div class="mb-5">
<label for="reg-lastname" class="mb-2 block text-sm font-medium text-white/80">
Vezetéknév
</label>
<input
id="reg-lastname"
v-model="lastName"
type="text"
placeholder="Vezetéknév"
autocomplete="family-name"
required
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
/>
</div>
<!-- First Name Input -->
<div class="mb-5">
<label for="reg-firstname" class="mb-2 block text-sm font-medium text-white/80">
Keresztnév
</label>
<input
id="reg-firstname"
v-model="firstName"
type="text"
placeholder="Keresztnév"
autocomplete="given-name"
required
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
/>
</div>
<!-- Email Input -->
<div class="mb-5">
<label for="reg-email" class="mb-2 block text-sm font-medium text-white/80">
E-mail
</label>
<input
id="reg-email"
v-model="regEmail"
type="email"
placeholder="pelda@email.com"
autocomplete="email"
required
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
/>
</div>
<!-- Password Input -->
<div class="mb-6 relative">
<label for="reg-password" class="mb-2 block text-sm font-medium text-white/80">
Jelszó
</label>
<input
id="reg-password"
v-model="regPassword"
:type="showRegPassword ? 'text' : 'password'"
placeholder="••••••••"
autocomplete="new-password"
required
minlength="8"
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 pr-12 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
/>
<!-- Eye icon (hold to show) -->
<button
type="button"
@mousedown="showRegPassword = true"
@mouseup="showRegPassword = false"
@mouseleave="showRegPassword = false"
@touchstart.prevent="showRegPassword = true"
@touchend="showRegPassword = false"
class="absolute right-3 top-[42px] text-white/40 hover:text-white/70 transition-colors cursor-pointer"
tabindex="-1"
aria-label="Jelszó megtekintése"
>
<!-- Eye open (password visible) -->
<svg
v-if="showRegPassword"
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
<path d="M1 1l22 22" />
</svg>
<!-- Eye closed (password hidden) -->
<svg
v-else
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</div>
<!-- Error Message -->
<div
v-if="authStore.error && authMode === 'register'"
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
>
{{ authStore.error }}
</div>
<!-- Register Button (Premium) -->
<button
type="submit"
:disabled="authStore.isLoading"
class="btn-premium w-full px-4 py-3 text-lg"
>
{{ authStore.isLoading ? 'Kérlek várj...' : 'Regisztráció' }}
</button>
<!-- Switch back to Login -->
<p class="mt-6 text-center text-sm text-white/50">
Már van garázsod?
<a
href="#"
@click.prevent="switchMode('login')"
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
>
Belépés
</a>
</p>
</form>
<!-- ==================== THIRD FACE: FORGOT PASSWORD ==================== -->
<form
v-if="activeBackFace === 'forgot'"
@submit.prevent="handleForgotPassword"
class="backface-hidden rotate-x-180 absolute inset-0 w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
>
<!-- Close Button (X) -->
<button
type="button"
@click="$emit('close')"
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<!-- Title -->
<h2 class="mb-6 text-center text-2xl font-bold text-white">
Új jelszó igénylése
</h2>
<!-- Description -->
<p class="mb-6 rounded-xl bg-[#1A6B5A]/20 px-4 py-3 text-center text-sm text-[#5EC4B0]">
Add meg az e-mail címed, és küldünk egy biztonsági linket.
</p>
<!-- Email Input -->
<div class="mb-6">
<label for="forgot-email" class="mb-2 block text-sm font-medium text-white/80">
E-mail
</label>
<input
id="forgot-email"
v-model="forgotEmail"
type="email"
placeholder="pelda@email.com"
autocomplete="email"
required
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
/>
</div>
<!-- Error Message -->
<div
v-if="authStore.error && authMode === 'forgot'"
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
>
{{ authStore.error }}
</div>
<!-- Send Link Button (Premium) -->
<button
type="submit"
:disabled="authStore.isLoading"
class="btn-premium w-full px-4 py-3 text-lg"
>
{{ authStore.isLoading ? 'Kérlek várj...' : 'Link Küldése' }}
</button>
<!-- Back to Login -->
<p class="mt-6 text-center text-sm text-white/50">
<a
href="#"
@click.prevent="switchMode('login')"
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
>
Vissza a belépéshez
</a>
</p>
</form>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const props = defineProps<{
visible: boolean
}>()
const emit = defineEmits<{
close: []
}>()
const router = useRouter()
const authStore = useAuthStore()
// ── Login form fields ──
const email = ref('')
const password = ref('')
// ── Register form fields ──
const lastName = ref('')
const firstName = ref('')
const regEmail = ref('')
const regPassword = ref('')
// ── Forgot password form fields ──
const forgotEmail = ref('')
// ── Password visibility states (hold-to-show) ──
const showPassword = ref(false)
const showRegPassword = ref(false)
// ── 3D Flip state machine ──
const authMode = ref<'login' | 'register' | 'forgot'>('login')
const activeBackFace = ref<'register' | 'forgot'>('register')
// ── Reset modal state when it closes ──
watch(
() => props.visible,
(newVal) => {
if (!newVal) {
// Reset view to login
authMode.value = 'login'
activeBackFace.value = 'register'
// Clear all form fields
email.value = ''
password.value = ''
lastName.value = ''
firstName.value = ''
regEmail.value = ''
regPassword.value = ''
forgotEmail.value = ''
// Clear store error
authStore.clearError()
}
}
)
// ── Clear store error when switching modes ──
watch(authMode, () => {
authStore.clearError()
})
function switchMode(mode: 'login' | 'register' | 'forgot') {
if (mode !== 'login') {
activeBackFace.value = mode
}
authMode.value = mode
}
// ── Smart Login Logic ──
async function handleLogin() {
try {
await authStore.login(email.value, password.value)
// After successful login, check KYC status
if (authStore.isKycComplete) {
router.push('/dashboard')
} else {
router.push('/complete-kyc')
}
// Close the modal
emit('close')
} catch {
// Error is already set in authStore.error — UI displays it automatically
}
}
// ── Register Logic ──
async function handleRegister() {
try {
await authStore.register({
email: regEmail.value,
password: regPassword.value,
first_name: firstName.value,
last_name: lastName.value,
})
// Registration successful — flip back to login so user can sign in
switchMode('login')
// Pre-fill the email field
email.value = regEmail.value
// Clear register fields
lastName.value = ''
firstName.value = ''
regEmail.value = ''
regPassword.value = ''
} catch {
// Error is already set in authStore.error
}
}
// ── Forgot Password Logic ──
async function handleForgotPassword() {
try {
await authStore.forgotPassword(forgotEmail.value)
// Success — flip back to login
switchMode('login')
forgotEmail.value = ''
} catch {
// Error is already set in authStore.error
}
}
</script>
<style scoped>
/* Slide-Fade Transition: enters from top-right, fades in — slower & more elegant */
.slide-fade-enter-active {
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.slide-fade-leave-active {
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.slide-fade-enter-from {
opacity: 0;
transform: translateX(48px) translateY(-48px) scale(0.95);
}
.slide-fade-leave-to {
opacity: 0;
transform: translateX(48px) translateY(-48px) scale(0.95);
}
/* 3D Card Flip Utilities */
.perspective-1000 {
perspective: 1000px;
}
.transform-style-3d {
transform-style: preserve-3d;
}
.backface-hidden {
backface-visibility: hidden;
}
.rotate-y-180 {
transform: rotateY(180deg);
}
.rotate-x-180 {
transform: rotateX(180deg);
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="flex items-center gap-4 relative -mb-8 z-50 group cursor-pointer">
<svg
viewBox="0 0 240 240"
class="w-24 h-24 drop-shadow-[0_10px_20px_rgba(0,0,0,0.5)] transition-transform duration-500 group-hover:scale-105"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="sf-green" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#418890" />
<stop offset="100%" stop-color="#79B085" />
</linearGradient>
<linearGradient id="sf-white" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFFFFF" />
<stop offset="100%" stop-color="#D1D5DB" />
</linearGradient>
</defs>
<path d="M 80 180 L 30 210" stroke="url(#sf-green)" stroke-width="8" stroke-linecap="round"/>
<circle cx="15" cy="219" r="4" fill="url(#sf-green)"/>
<path d="M 110 205 L 40 245" stroke="url(#sf-green)" stroke-width="12" stroke-linecap="round"/>
<circle cx="20" cy="255" r="6" fill="url(#sf-green)"/>
<path d="M 140 225 L 90 255" stroke="url(#sf-green)" stroke-width="6" stroke-linecap="round"/>
<circle cx="75" cy="264" r="3" fill="url(#sf-green)"/>
<polygon points="120,30 108,50 132,50" fill="url(#sf-white)"/>
<polygon points="120,210 108,190 132,190" fill="url(#sf-white)"/>
<polygon points="30,120 50,108 50,132" fill="url(#sf-white)"/>
<polygon points="65,65 85,78 70,88" fill="url(#sf-white)"/>
<circle cx="120" cy="120" r="70" stroke="url(#sf-white)" stroke-width="14"/>
<text x="90" y="155" font-family="Arial, Helvetica, sans-serif" font-weight="900" font-size="95" text-anchor="middle" fill="url(#sf-white)">S</text>
<text x="155" y="155" font-family="Arial, Helvetica, sans-serif" font-weight="900" font-size="95" text-anchor="middle" fill="url(#sf-green)">F</text>
<path d="M 170 170 L 215 215" stroke="url(#sf-white)" stroke-width="26" stroke-linecap="round"/>
<path d="M 180 180 L 210 210" stroke="#062535" stroke-width="6" stroke-linecap="round"/>
<circle cx="125" cy="110" r="15" fill="url(#sf-white)"/>
<circle cx="125" cy="110" r="6" fill="#062535"/>
<polygon points="215,25 125,110 95,70" fill="#2C5A63"/>
<polygon points="215,25 125,110 165,140" fill="url(#sf-green)"/>
</svg>
<div class="hidden sm:flex flex-col justify-center mt-3">
<div class="font-black tracking-widest text-3xl leading-none drop-shadow-md">
<span class="text-white">SERVICE</span> <span class="text-[#75A882]">FINDER</span>
</div>
<div class="text-[#75A882] text-[0.65rem] tracking-[0.25em] font-bold mt-2 uppercase opacity-90">
Online Járműnyilvántartó & Szervizkereső
</div>
</div>
</div>
</template>
<script setup>
// Tiszta, újrahasználható, kézzel optimalizált SVG logó.
</script>

View File

@@ -1,161 +0,0 @@
<template>
<div class="max-w-6xl mx-auto p-6">
<h1 class="text-4xl font-bold text-center mb-4 text-slate-900">Welcome to Service Finder</h1>
<p class="text-lg text-slate-600 text-center mb-12 max-w-2xl mx-auto">
Choose your experience based on how you use vehicles. Your selection will customize the dashboard and features.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
<!-- Private Garage Card - Vibrant gradient border & playful -->
<div
class="relative rounded-3xl p-8 cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-95 bg-gradient-to-br from-white to-slate-50/80 backdrop-blur-sm border border-slate-200/60"
:class="{
'selected shadow-2xl ring-4 ring-opacity-30 ring-amber-400/50': isPrivateGarage,
'private-garage': true
}"
@click="selectMode('private_garage')"
>
<!-- Vibrant gradient border effect -->
<div v-if="isPrivateGarage" class="absolute -inset-0.5 bg-gradient-to-r from-amber-400 via-orange-400 to-pink-400 rounded-3xl blur-sm opacity-70 -z-10"></div>
<div class="mb-6 p-4 rounded-2xl inline-flex bg-gradient-to-br from-amber-100 to-orange-100 text-amber-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
</div>
<h2 class="text-2xl font-bold mb-4 text-slate-900">Private Garage</h2>
<p class="text-slate-700 mb-6 leading-relaxed">
Perfect for individual vehicle owners. Track expenses, maintenance, and get personalized recommendations for your personal cars, motorcycles, or recreational vehicles.
</p>
<ul class="mb-8 space-y-3 card-features-private">
<li class="flex items-center text-sm text-slate-700">Personal vehicle management</li>
<li class="flex items-center text-sm text-slate-700">Expense tracking & budgeting</li>
<li class="flex items-center text-sm text-slate-700">Maintenance reminders</li>
<li class="flex items-center text-sm text-slate-700">Fuel efficiency analytics</li>
</ul>
<div class="flex justify-between items-center">
<span class="px-3 py-1.5 rounded-full text-xs font-semibold bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 border border-amber-200/60">For Individuals</span>
<button
class="px-6 py-2.5 rounded-lg font-medium transition-all duration-200 bg-gradient-to-r from-amber-500 to-orange-500 text-white hover:from-amber-600 hover:to-orange-600 active:scale-95 shadow-md hover:shadow-lg"
:class="{ 'ring-2 ring-amber-300 ring-offset-2': isPrivateGarage }"
>
{{ isPrivateGarage ? '✓ Selected' : 'Select' }}
</button>
</div>
</div>
<!-- Corporate Fleet Card - Minimalist sharp design -->
<div
class="relative rounded-3xl p-8 cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-95 bg-gradient-to-br from-white to-slate-50/80 backdrop-blur-sm border border-slate-200/60"
:class="{
'selected shadow-2xl ring-4 ring-opacity-30 ring-blue-400/50': isCorporateFleet,
'corporate-fleet': true
}"
@click="selectMode('corporate_fleet')"
>
<!-- Sharp business accent line -->
<div v-if="isCorporateFleet" class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-t-3xl"></div>
<div class="mb-6 p-4 rounded-2xl inline-flex bg-gradient-to-br from-blue-50 to-cyan-50 text-blue-700 border border-blue-200/40">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<h2 class="text-2xl font-bold mb-4 text-slate-900">Corporate Fleet</h2>
<p class="text-slate-700 mb-6 leading-relaxed">
Designed for fleet managers and businesses. Monitor multiple vehicles, optimize TCO (Total Cost of Ownership), and manage service schedules across your entire fleet.
</p>
<ul class="mb-8 space-y-3 card-features-corporate">
<li class="flex items-center text-sm text-slate-700">Multi-vehicle fleet management</li>
<li class="flex items-center text-sm text-slate-700">TCO & ROI analytics</li>
<li class="flex items-center text-sm text-slate-700">Driver assignment & reporting</li>
<li class="flex items-center text-sm text-slate-700">Bulk service scheduling</li>
</ul>
<div class="flex justify-between items-center">
<span class="px-3 py-1.5 rounded-full text-xs font-semibold bg-gradient-to-r from-blue-50 to-cyan-50 text-blue-800 border border-blue-200/60">For Businesses</span>
<button
class="px-6 py-2.5 rounded-lg font-medium transition-all duration-200 bg-gradient-to-r from-blue-600 to-cyan-600 text-white hover:from-blue-700 hover:to-cyan-700 active:scale-95 shadow-md hover:shadow-lg"
:class="{ 'ring-2 ring-blue-300 ring-offset-2': isCorporateFleet }"
>
{{ isCorporateFleet ? '✓ Selected' : 'Select' }}
</button>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row justify-between items-center p-6 border-t border-slate-200/60">
<p class="text-sm text-slate-500">
You can change this later from the header menu.
</p>
<button
class="mt-4 md:mt-0 px-8 py-3.5 bg-gradient-to-r from-blue-600 to-cyan-600 text-white font-semibold rounded-xl hover:from-blue-700 hover:to-cyan-700 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center shadow-lg hover:shadow-xl active:scale-95"
@click="continueToDashboard"
:disabled="!mode"
>
Continue to Dashboard
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</template>
<script setup>
import { useAppModeStore } from '@/stores/appModeStore'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
const appModeStore = useAppModeStore()
const router = useRouter()
const { mode, isPrivateGarage, isCorporateFleet } = storeToRefs(appModeStore)
function selectMode(newMode) {
appModeStore.setMode(newMode)
}
function continueToDashboard() {
console.log('ProfileSelector: Continuing to dashboard with mode', mode.value)
try {
router.push('/')
console.log('ProfileSelector: Redirect to dashboard successful')
} catch (error) {
console.error('ProfileSelector: Failed to redirect to dashboard:', error)
}
}
</script>
<style scoped>
/* Keep only pseudo-element and media query styles */
.card-features-private li::before,
.card-features-corporate li::before {
content: '✓';
margin-right: 0.5rem;
font-weight: bold;
}
.card-features-private li::before {
color: #f59e0b; /* amber-500 */
}
.card-features-corporate li::before {
color: #06b6d4; /* cyan-500 */
}
/* Responsive adjustments */
@media (max-width: 768px) {
.grid {
gap: 1.5rem;
}
.rounded-3xl.p-8 {
padding: 1.5rem;
}
.flex.flex-col.md\:flex-row {
flex-direction: column;
gap: 1rem;
}
}
</style>

View File

@@ -1,245 +0,0 @@
<script setup>
import { ref } from 'vue'
import { useExpenseStore } from '@/stores/expenseStore'
import { useGarageStore } from '@/stores/garageStore'
const emit = defineEmits(['close'])
const expenseStore = useExpenseStore()
const garageStore = useGarageStore()
// Use selected vehicle from garage store, or default to first vehicle
const selectedAssetId = ref(garageStore.selectedVehicle?.id || garageStore.vehicles[0]?.id || '')
const amount = ref('')
const category = ref('fuel')
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) {
alert('Nincs kiválasztott jármű. Kérjük, először adj hozzá egy járművet.')
return
}
if (!amount.value || !date.value) {
alert('Kérjük, töltsd ki a kötelező mezőket.')
return
}
isLoading.value = true
try {
const expenseData = {
asset_id: selectedAssetId.value,
cost_type: category.value, // fuel, service, tax, insurance
amount_local: parseFloat(amount.value),
currency_local: 'HUF', // default, could be dynamic
date: new Date(date.value).toISOString(),
description: description.value,
mileage_at_cost: mileage.value ? parseInt(mileage.value) : null,
data: {}
}
await expenseStore.createExpense(expenseData)
// Success
alert('Költség sikeresen mentve!')
// Reset form
amount.value = ''
category.value = 'fuel'
date.value = new Date().toISOString().split('T')[0]
description.value = ''
mileage.value = ''
// Close modal
emit('close')
} catch (error) {
console.error('Error saving expense:', error)
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
}
}
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>
<!-- Modal Backdrop -->
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black bg-opacity-50 p-4" @click.self="closeModal">
<!-- Modal Container -->
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
<!-- Modal Header -->
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">Költség / Üzemanyag hozzáadása</h2>
<button @click="closeModal" class="text-gray-500 hover:text-gray-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Modal Body -->
<div class="p-6">
<form @submit.prevent="handleSubmit" class="space-y-5">
<!-- Asset selection (if multiple vehicles) -->
<div v-if="garageStore.vehicles.length > 0">
<label class="block text-sm font-medium text-gray-700 mb-2">Jármű</label>
<select
v-model="selectedAssetId"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
>
<option v-for="vehicle in garageStore.vehicles" :key="vehicle.id" :value="vehicle.id">
{{ vehicle.name || vehicle.license_plate || vehicle.vin }}
</option>
</select>
</div>
<div v-else class="text-sm text-amber-600 bg-amber-50 p-3 rounded-lg">
Nincs még járműved. Először adj hozzá egy járművet a "Jármű Hozzáadása" gombbal.
</div>
<!-- Amount -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Összeg (HUF)</label>
<input
v-model="amount"
type="number"
step="0.01"
min="0"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="0.00"
/>
</div>
<!-- Category -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Kategória</label>
<select
v-model="category"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
>
<option value="fuel">Üzemanyag</option>
<option value="service">Szerviz / Karbantartás</option>
<option value="tax">Adó / Díj</option>
<option value="insurance">Biztosítás</option>
<option value="parking">Parkolás</option>
<option value="toll">Útdíj</option>
<option value="other">Egyéb</option>
</select>
</div>
<!-- Date -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Dátum</label>
<input
v-model="date"
type="date"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
/>
</div>
<!-- Mileage (optional) -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Kilométeróra állása (opcionális)</label>
<input
v-model="mileage"
type="number"
min="0"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="pl. 123456"
/>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Leírás (opcionális)</label>
<textarea
v-model="description"
rows="3"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="Pl.: Tankolás, olajcsere..."
></textarea>
</div>
<!-- Buttons -->
<div class="flex gap-3 pt-4">
<button
type="button"
@click="closeModal"
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition"
>
Mégse
</button>
<button
type="submit"
:disabled="isLoading || !selectedAssetId"
class="flex-1 px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
>
<span v-if="isLoading">Mentés...</span>
<span v-else>Mentés</span>
</button>
</div>
</form>
</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>
/* Additional custom styles if needed */
</style>

View File

@@ -1,404 +0,0 @@
<script setup>
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
try {
// Prepare vehicle data for the API
const vehicleData = {
make: make.value,
model: model.value,
licensePlate: licensePlate.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
await garageStore.addVehicle(vehicleData)
// Show success message
alert('Sikeres mentés! Jármű hozzáadva.')
// 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')
} catch (err) {
console.error('Error adding vehicle:', err)
error.value = err.message || 'Ismeretlen hiba történt'
alert(`Hiba: ${error.value}`)
} finally {
isLoading.value = false
}
}
const closeModal = () => {
emit('close')
}
</script>
<template>
<!-- Modal Backdrop -->
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black bg-opacity-50 p-4" @click.self="closeModal">
<!-- Modal Container -->
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
<!-- Modal Header -->
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">Jármű hozzáadása</h2>
<button @click="closeModal" class="text-gray-500 hover:text-gray-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Modal Body -->
<div class="p-6">
<form @submit.prevent="handleSubmit" class="space-y-5">
<!-- Make -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Gyártó</label>
<input
v-model="make"
type="text"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
placeholder="Pl.: Toyota"
/>
</div>
<!-- Model -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Modell</label>
<input
v-model="model"
type="text"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
placeholder="Pl.: Corolla"
/>
</div>
<!-- License Plate -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Rendszám</label>
<input
v-model="licensePlate"
type="text"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
placeholder="Pl.: ABC-123"
/>
</div>
<!-- Year and Fuel Type in a grid -->
<div class="grid grid-cols-2 gap-4">
<!-- Year -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Évjárat</label>
<input
v-model="year"
type="number"
min="1900"
:max="new Date().getFullYear() + 1"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
placeholder="2023"
/>
</div>
<!-- Fuel Type -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Üzemanyag típus</label>
<select
v-model="fuelType"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900"
>
<option value="petrol">Benzin</option>
<option value="diesel">Dízel</option>
<option value="electric">Elektromos</option>
<option value="hybrid">Hibrid</option>
<option value="lpg">LPG</option>
<option value="other">Egyéb</option>
</select>
</div>
</div>
<!-- Error Message -->
<div v-if="error" class="p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg">
{{ error }}
</div>
<!-- Buttons -->
<div class="flex gap-3 pt-4">
<button
type="button"
@click="closeModal"
:disabled="isLoading"
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
Mégse
</button>
<button
type="submit"
:disabled="isLoading"
class="flex-1 px-4 py-3 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
<span v-if="isLoading" class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-white border-r-transparent mr-2"></span>
{{ isLoading ? 'Feldolgozás...' : 'Jármű hozzáadása' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<style scoped>
/* Additional custom styles if needed */
</style>

View File

@@ -1,135 +0,0 @@
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['close'])
const serviceType = ref('maintenance')
const location = ref('')
const urgency = ref('medium')
const handleSubmit = () => {
// In a real app, you would call an API here
console.log('Service search submitted:', {
serviceType: serviceType.value,
location: location.value,
urgency: urgency.value
})
// Show success message
alert('Szerviz keresés elindítva! Hamarosan értesítünk.')
// Reset form
serviceType.value = 'maintenance'
location.value = ''
urgency.value = 'medium'
// Close modal
emit('close')
}
const closeModal = () => {
emit('close')
}
</script>
<template>
<!-- Modal Backdrop -->
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black bg-opacity-50 p-4" @click.self="closeModal">
<!-- Modal Container -->
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
<!-- Modal Header -->
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">Szerviz Keresése</h2>
<button @click="closeModal" class="text-gray-500 hover:text-gray-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Modal Body -->
<div class="p-6">
<form @submit.prevent="handleSubmit" class="space-y-5">
<!-- Service Type -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Szerviz típusa</label>
<select
v-model="serviceType"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
>
<option value="maintenance">Általános karbantartás</option>
<option value="repair">Javítás</option>
<option value="diagnostic">Diagnosztika</option>
<option value="tire">Gumiszerviz</option>
<option value="oil">Olajcsere</option>
<option value="brake">Fékrendszer</option>
<option value="other">Egyéb</option>
</select>
</div>
<!-- Location -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Helyszín (város/irányítószám)</label>
<input
v-model="location"
type="text"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="Pl.: Budapest"
/>
</div>
<!-- Urgency -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Sürgősség</label>
<div class="flex gap-4">
<label class="flex items-center">
<input v-model="urgency" type="radio" value="low" class="mr-2">
<span class="text-gray-700">Alacsony</span>
</label>
<label class="flex items-center">
<input v-model="urgency" type="radio" value="medium" class="mr-2">
<span class="text-gray-700">Közepes</span>
</label>
<label class="flex items-center">
<input v-model="urgency" type="radio" value="high" class="mr-2">
<span class="text-gray-700">Magas</span>
</label>
</div>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Részletes leírás (opcionális)</label>
<textarea
rows="3"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
placeholder="Pl.: Motorhiba, féknyikorgás..."
></textarea>
</div>
<!-- Buttons -->
<div class="flex gap-3 pt-4">
<button
type="button"
@click="closeModal"
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition"
>
Mégse
</button>
<button
type="submit"
class="flex-1 px-4 py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition"
>
Szerviz keresése
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<style scoped>
/* Additional custom styles if needed */
</style>

View File

@@ -1,115 +0,0 @@
<script setup>
import { ref } from 'vue'
import AddExpenseModal from './AddExpenseModal.vue'
import SmartVehicleRegistration from './SmartVehicleRegistration.vue'
import FindServiceModal from './FindServiceModal.vue'
const isOpen = ref(false)
const showExpenseModal = ref(false)
const showSmartVehicleRegistration = ref(false)
const showServiceModal = ref(false)
const toggleMenu = () => {
isOpen.value = !isOpen.value
}
const openExpenseModal = () => {
showExpenseModal.value = true
isOpen.value = false
}
const openSmartVehicleRegistration = () => {
showSmartVehicleRegistration.value = true
isOpen.value = false
}
const openServiceModal = () => {
showServiceModal.value = true
isOpen.value = false
}
const closeExpenseModal = () => {
showExpenseModal.value = false
}
const closeSmartVehicleRegistration = () => {
showSmartVehicleRegistration.value = false
}
const closeServiceModal = () => {
showServiceModal.value = false
}
</script>
<template>
<!-- Floating Action Button -->
<div class="fixed bottom-6 right-6 z-50 flex flex-col items-end">
<!-- Action Menu (shown when open) -->
<div v-if="isOpen" class="mb-4 space-y-3">
<!-- Add Expense Button -->
<button
@click="openExpenseModal"
class="flex items-center justify-end gap-3 bg-blue-600 hover:bg-blue-700 text-white px-4 py-3 rounded-full shadow-lg transition-all duration-200 transform hover:scale-105"
>
<span class="text-sm font-semibold">Költség / Üzemanyag</span>
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
</button>
<!-- Find Service Button -->
<button
@click="openServiceModal"
class="flex items-center justify-end gap-3 bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-full shadow-lg transition-all duration-200 transform hover:scale-105"
>
<span class="text-sm font-semibold">Szerviz Keresése</span>
<div class="w-10 h-10 flex items-center justify-center bg-green-800 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</button>
<!-- Add Vehicle Button -->
<button
@click="openSmartVehicleRegistration"
class="flex items-center justify-end gap-3 bg-purple-600 hover:bg-purple-700 text-white px-4 py-3 rounded-full shadow-lg transition-all duration-200 transform hover:scale-105"
>
<span class="text-sm font-semibold">Jármű Hozzáadása</span>
<div class="w-10 h-10 flex items-center justify-center bg-purple-800 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
</button>
</div>
<!-- Main FAB Button -->
<button
@click="toggleMenu"
class="w-14 h-14 flex items-center justify-center bg-blue-700 hover:bg-blue-800 text-white rounded-full shadow-xl transition-all duration-200 transform hover:scale-110"
:class="{ 'rotate-45': isOpen }"
>
<svg v-if="!isOpen" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Modals -->
<AddExpenseModal v-if="showExpenseModal" @close="closeExpenseModal" />
<SmartVehicleRegistration v-if="showSmartVehicleRegistration" @close="closeSmartVehicleRegistration" />
<FindServiceModal v-if="showServiceModal" @close="closeServiceModal" />
</template>
<style scoped>
/* Smooth transitions */
button {
transition: all 0.2s ease-in-out;
}
</style>

View File

@@ -1,964 +0,0 @@
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useGarageStore } from '../../stores/garageStore'
import { useAuthStore } from '../../stores/authStore'
import { useAppModeStore } from '../../stores/appModeStore'
import { catalogApi, organizationApi } from '../../services/api'
const emit = defineEmits(['close', 'success'])
// Store instances
const garageStore = useGarageStore()
const authStore = useAuthStore()
const appModeStore = useAppModeStore()
// Organization context
const selectedOrganizationId = ref(null)
const userOrganizations = ref([])
const loadingOrganizations = ref(false)
// Owner vs Operator toggle
const ownershipType = ref('owner') // 'owner' or 'operator'
// Wizard steps - now 4 steps total (0: Organization, 1: Classification, 2: Catalog, 3: Details)
const currentStep = ref(0)
const totalSteps = 4
// Check if organization step is needed
const needsOrganizationSelection = computed(() => {
// Always show in fleet mode, or if user has multiple organizations
return isFleetMode.value || (userOrganizations.value && userOrganizations.value.length > 1)
})
// Step 0: Organization Selection - Categorized
const categorizedOrganizations = computed(() => {
const privateOrgs = []
const businessOrgs = []
if (userOrganizations.value && userOrganizations.value.length > 0) {
userOrganizations.value.forEach(org => {
// Check if organization has tax_number or org_type is not 'individual'
// Note: org_type values: 'individual', 'company', 'non_profit', 'government'
const isBusiness = org.tax_number || (org.org_type && org.org_type !== 'individual')
if (isBusiness) {
businessOrgs.push({
id: org.organization_id,
name: org.display_name || org.name,
description: org.full_name || 'Céges flotta',
type: 'business',
originalOrg: org
})
} else {
privateOrgs.push({
id: org.organization_id,
name: 'Személyes Garázsom',
description: 'Személyes járművek',
type: 'private',
originalOrg: org
})
}
})
}
return { privateOrgs, businessOrgs }
})
// Backward compatibility - flat list of all organizations
const organizationOptions = computed(() => {
const options = []
const { privateOrgs, businessOrgs } = categorizedOrganizations.value
// Add private organizations first
privateOrgs.forEach(org => {
options.push(org)
})
// Add business organizations
businessOrgs.forEach(org => {
options.push(org)
})
return options
})
// Step 1: Classification
const vehicleClass = ref('')
const vehicleClasses = ref([
{ value: 'passenger_car', label: 'Személygépkocsi', icon: '🚗' },
{ value: 'motorcycle', label: 'Motorkerékpár', icon: '🏍️' },
{ value: 'commercial_vehicle', label: 'Tehergépkocsi', icon: '🚚' },
{ value: 'bus', label: 'Busz', icon: '🚌' },
{ value: 'special_vehicle', label: 'Különleges jármű', icon: '🚜' }
])
// Step 2: Catalog Auto-Complete
const make = ref('')
const model = ref('')
const generation = ref('')
const engine = ref('')
const catalogId = 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)
// Step 3: Unique Details
const licensePlate = ref('')
const vin = ref('')
const currentMileage = ref('')
const color = ref('')
// Form state
const isLoading = ref(false)
const error = ref(null)
const successMessage = ref('')
// Progress indicator
const progressPercentage = ref(0)
// UI Mode styling
const uiMode = computed(() => appModeStore.mode)
const isFleetMode = computed(() => uiMode.value === 'fleet')
const isPersonalMode = computed(() => uiMode.value === 'personal')
// Computed properties for empty states
const hasGenerations = computed(() => generations.value && generations.value.length > 0)
const hasEngines = computed(() => engines.value && engines.value.length > 0)
const generationsLoaded = computed(() => !loadingGenerations.value)
const enginesLoaded = computed(() => !loadingEngines.value)
// Step validation - handle empty generations/engines gracefully
const canProceedToNextStep = computed(() => {
if (currentStep.value === 0) {
// Organization step: only required if selection is needed
if (needsOrganizationSelection.value) {
// Must have a valid organization ID (not null, not undefined)
return selectedOrganizationId.value != null
}
return true // Skip if not needed
} else if (currentStep.value === 1) {
return !!vehicleClass.value
} else if (currentStep.value === 2) {
// Basic requirements
if (!make.value || !model.value) return false
// If generations are still loading, wait
if (loadingGenerations.value) return false
// Check if generations are available
if (hasGenerations.value) {
// If generations exist, require generation selection
if (!generation.value) return false
// If engines are still loading, wait
if (loadingEngines.value) return false
// Check if engines are available for the selected generation
if (hasEngines.value) {
// If engines exist, require engine selection
if (!engine.value) return false
}
// If no engines available, generation selection is enough
return true
}
// If no generations available, make and model are enough
return true
}
return true
})
// Calculate progress based on filled fields
const calculateProgress = () => {
let filledFields = 0
let totalFields = 0
// Step 0: Organization (if needed)
if (needsOrganizationSelection.value) {
if (selectedOrganizationId.value !== undefined) filledFields++
totalFields++
}
// Step 1 fields
if (vehicleClass.value) filledFields++
totalFields++
// Step 2 fields (weighted more)
if (make.value) filledFields++
if (model.value) filledFields++
if (generation.value) filledFields++
if (engine.value) filledFields++
totalFields += 4
// Step 3 fields
if (licensePlate.value) filledFields++
if (vin.value) filledFields++
if (currentMileage.value) filledFields++
totalFields += 3
progressPercentage.value = Math.round((filledFields / totalFields) * 100)
}
// Watch all form fields for progress updates
watch([selectedOrganizationId, vehicleClass, make, model, generation, engine, licensePlate, vin, currentMileage], () => {
calculateProgress()
})
// Fetch organizations and makes on component mount
onMounted(async () => {
await fetchUserOrganizations()
await fetchMakes()
calculateProgress()
})
// Fetch user's organizations
async function fetchUserOrganizations() {
if (!authStore.isLoggedIn) return
loadingOrganizations.value = true
try {
const data = await organizationApi.getMyOrganizations()
userOrganizations.value = data
// If user has only one organization and is in fleet mode, auto-select it
if (isFleetMode.value && data.length === 1) {
selectedOrganizationId.value = data[0].organization_id
}
} catch (err) {
console.error('Error fetching organizations:', err)
// Non-critical error, continue without organizations
} finally {
loadingOrganizations.value = false
}
}
// Fetch makes from catalog API
async function fetchMakes() {
loadingMakes.value = true
try {
const data = await catalogApi.getMakes()
makes.value = data
} catch (err) {
console.error('Error fetching makes:', err)
error.value = 'Could not load vehicle makes. Please try again.'
} finally {
loadingMakes.value = false
}
}
// Fetch models when make is selected
watch(make, async (newMake) => {
if (newMake) {
loadingModels.value = true
try {
// Pass vehicle_class to filter models by selected vehicle class
const data = await catalogApi.getModels(newMake, vehicleClass.value)
models.value = data
model.value = ''
generation.value = ''
engine.value = ''
catalogId.value = null
generations.value = []
engines.value = []
} catch (err) {
console.error('Error fetching models:', err)
error.value = 'Could not load models for this make.'
} finally {
loadingModels.value = false
}
} else {
models.value = []
generations.value = []
engines.value = []
}
})
// Fetch generations when model is selected
watch(model, async (newModel) => {
if (newModel && make.value) {
loadingGenerations.value = true
try {
const data = await catalogApi.getGenerations(make.value, newModel)
generations.value = data
generation.value = ''
engine.value = ''
catalogId.value = null
engines.value = []
} catch (err) {
console.error('Error fetching generations:', err)
error.value = 'Could not load generations for this model.'
} finally {
loadingGenerations.value = false
}
} else {
generations.value = []
engines.value = []
}
})
// Fetch engines when generation is selected
watch(generation, async (newGen) => {
if (newGen && make.value && model.value) {
loadingEngines.value = true
try {
const data = await catalogApi.getEngines(make.value, model.value, newGen)
engines.value = data
engine.value = ''
catalogId.value = null
} catch (err) {
console.error('Error fetching engines:', err)
error.value = 'Could not load engines for this generation.'
} finally {
loadingEngines.value = false
}
} else {
engines.value = []
}
})
// Set catalog ID when engine is selected
watch(engine, (newEngine) => {
if (newEngine) {
const selectedEngine = engines.value.find(e => e.variant === newEngine || e.id === newEngine)
if (selectedEngine) {
catalogId.value = selectedEngine.id
}
} else {
catalogId.value = null
}
})
// Navigation functions
function nextStep() {
if (currentStep.value < totalSteps) {
currentStep.value++
}
}
function prevStep() {
if (currentStep.value > 1) {
currentStep.value--
}
}
// Form submission
async function handleSubmit() {
error.value = null
successMessage.value = ''
isLoading.value = true
try {
// Determine organization ID: use selected organization, or auth store's active org, or null for personal
const organizationId = selectedOrganizationId.value !== null
? selectedOrganizationId.value
: (isFleetMode.value ? authStore.activeOrgId : null)
// Determine owner and operator org IDs based on ownership type
let ownerOrgId = null
let operatorOrgId = null
if (organizationId !== null) {
if (ownershipType.value === 'owner') {
// Owner: both owner and operator are the selected organization
ownerOrgId = organizationId
operatorOrgId = organizationId
} else {
// Operator: only operator is set, owner remains null
operatorOrgId = organizationId
// ownerOrgId stays null
}
}
// Prepare thick asset payload
const vehicleData = {
// Core identification
vin: vin.value || null,
licensePlate: licensePlate.value,
catalogId: catalogId.value,
organizationId: organizationId,
// Ownership fields
owner_org_id: ownerOrgId,
operator_org_id: operatorOrgId,
// Thick Asset fields
brand: make.value,
model: model.value,
vehicleClass: vehicleClass.value,
fuelType: getFuelTypeFromEngine(),
year: getYearFromGeneration(),
currentMileage: parseInt(currentMileage.value) || 0,
color: color.value,
// Additional metadata
status: 'draft',
generation: generation.value,
engine: engine.value
}
// Call the updated garage store addVehicle action
const result = await garageStore.addVehicle(vehicleData)
successMessage.value = 'Vehicle registered successfully! The backend will now sync catalog data and calculate profile completion.'
// Emit success event
emit('success', result)
// Reset form after a delay
setTimeout(() => {
resetForm()
emit('close')
}, 2000)
} catch (err) {
console.error('Error registering vehicle:', err)
error.value = err.message || 'An unknown error occurred'
} finally {
isLoading.value = false
}
}
// Helper functions
function getFuelTypeFromEngine() {
if (!engine.value || !engines.value.length) return null
const selected = engines.value.find(e => e.variant === engine.value || e.id === engine.value)
return selected?.fuel_type || null
}
function getYearFromGeneration() {
// Extract year from generation string (e.g., "2015-2019" or "2020")
if (!generation.value) return null
const match = generation.value.match(/\d{4}/)
return match ? parseInt(match[0]) : null
}
function resetForm() {
currentStep.value = needsOrganizationSelection.value ? 0 : 1
selectedOrganizationId.value = null
ownershipType.value = 'owner' // Reset to default
vehicleClass.value = ''
make.value = ''
model.value = ''
generation.value = ''
engine.value = ''
catalogId.value = null
licensePlate.value = ''
vin.value = ''
currentMileage.value = ''
color.value = ''
error.value = null
successMessage.value = ''
progressPercentage.value = 0
}
function closeModal() {
emit('close')
}
</script>
<template>
<!-- Modal Backdrop -->
<div class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black bg-opacity-50" @click.self="closeModal">
<!-- Modal Container -->
<div class="w-full max-w-2xl overflow-hidden bg-white rounded-xl shadow-2xl" :class="isFleetMode ? 'border-l-4 border-blue-600' : 'border-l-4 border-green-600'">
<!-- Modal Header -->
<div class="p-6 border-b border-gray-200" :class="isFleetMode ? 'bg-blue-50' : 'bg-green-50'">
<div class="flex items-center justify-between">
<div>
<h2 class="text-xl font-bold text-gray-900">
Smart Vehicle Registration
</h2>
<p class="text-sm mt-1 text-gray-600">
{{ isFleetMode ? 'Add a new vehicle to your fleet' : 'Register your vehicle in just 3 easy steps!' }}
</p>
</div>
<button @click="closeModal" class="p-2 rounded-full text-gray-500 hover:text-gray-700 hover:bg-gray-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Progress Indicator -->
<div class="mt-6">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700">Progress</span>
<span class="text-sm font-bold" :class="isFleetMode ? 'text-blue-600' : 'text-green-600'">{{ progressPercentage }}%</span>
</div>
<div class="h-2 rounded-full overflow-hidden bg-gray-200">
<div
class="h-full transition-all duration-500"
:class="isFleetMode ? 'bg-blue-600' : 'bg-green-600'"
:style="{ width: `${progressPercentage}%` }"
></div>
</div>
<!-- Step Indicators -->
<div class="flex justify-between mt-4">
<div
v-for="step in totalSteps"
:key="step"
class="flex flex-col items-center"
>
<div
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-all duration-300"
:class="[
step === currentStep
? (isFleetMode ? 'bg-blue-600 text-white ring-2 ring-blue-300' : 'bg-green-600 text-white ring-2 ring-green-300')
: step < currentStep
? (isFleetMode ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700')
: 'bg-gray-100 text-gray-400'
]"
>
{{ step }}
</div>
<span class="text-xs mt-1 font-medium" :class="[
step === currentStep
? (isFleetMode ? 'text-blue-700' : 'text-green-700')
: step < currentStep
? (isFleetMode ? 'text-blue-600' : 'text-green-600')
: 'text-gray-500'
]">
{{ step === 0 ? 'Context' : step === 1 ? 'Classification' : step === 2 ? 'Catalog' : 'Details' }}
</span>
</div>
</div>
</div>
</div>
<!-- Modal Body -->
<div class="p-6 max-h-[60vh] overflow-y-auto">
<!-- Step 0: Organization Selection (if needed) -->
<div v-if="currentStep === 0 && needsOrganizationSelection">
<h3 class="text-lg font-semibold mb-4 text-gray-800">1. lépés: Szervezet kiválasztása</h3>
<p class="text-sm mb-6 text-gray-600">
Válaszd ki, hogy melyik szervezethez szeretnéd regisztrálni a járművet. A személyes garázsodhoz vagy egy céges flottához.
</p>
<div class="space-y-6">
<!-- Private Organizations (Személyes Garázsom) -->
<div v-if="categorizedOrganizations.privateOrgs.length > 0">
<h4 class="text-sm font-semibold text-gray-700 mb-3">Személyes Garázsom</h4>
<div class="space-y-3">
<div
v-for="org in categorizedOrganizations.privateOrgs"
:key="org.id"
@click="selectedOrganizationId = org.id"
class="p-4 rounded-lg border-2 transition-all duration-200 cursor-pointer"
:class="selectedOrganizationId === org.id
? (isFleetMode ? 'border-blue-500 bg-blue-50' : 'border-green-500 bg-green-50')
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50'"
>
<div class="flex items-center">
<div class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center mr-4"
:class="selectedOrganizationId === org.id
? (isFleetMode ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600')
: 'bg-gray-100 text-gray-500'">
<span>🏠</span>
</div>
<div class="flex-grow">
<h4 class="font-medium text-gray-900">{{ org.name }}</h4>
<p class="text-sm text-gray-600">{{ org.description }}</p>
</div>
<div v-if="selectedOrganizationId === org.id" class="text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
</div>
</div>
</div>
<!-- Business Organizations (Céges Flották) -->
<div v-if="categorizedOrganizations.businessOrgs.length > 0">
<h4 class="text-sm font-semibold text-gray-700 mb-3">Céges Flották</h4>
<div class="space-y-3">
<div
v-for="org in categorizedOrganizations.businessOrgs"
:key="org.id"
@click="selectedOrganizationId = org.id"
class="p-4 rounded-lg border-2 transition-all duration-200 cursor-pointer"
:class="selectedOrganizationId === org.id
? (isFleetMode ? 'border-blue-500 bg-blue-50' : 'border-green-500 bg-green-50')
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50'"
>
<div class="flex items-center">
<div class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center mr-4"
:class="selectedOrganizationId === org.id
? (isFleetMode ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600')
: 'bg-gray-100 text-gray-500'">
<span>🏢</span>
</div>
<div class="flex-grow">
<h4 class="font-medium text-gray-900">{{ org.name }}</h4>
<p class="text-sm text-gray-600">{{ org.description }}</p>
<p v-if="org.originalOrg.tax_number" class="text-xs text-gray-500 mt-1">
Adószám: {{ org.originalOrg.tax_number }}
</p>
</div>
<div v-if="selectedOrganizationId === org.id" class="text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
</div>
</div>
</div>
<!-- No organizations message -->
<div v-if="!loadingOrganizations && categorizedOrganizations.privateOrgs.length === 0 && categorizedOrganizations.businessOrgs.length === 0" class="text-center p-6 border border-dashed border-gray-300 rounded-lg">
<p class="text-gray-600">Nincs elérhető szervezet. Először hozz létre egy személyes garázst vagy céges flottát a profilodban.</p>
</div>
<!-- Owner vs Operator Toggle (shown when organization is selected) -->
<div v-if="selectedOrganizationId !== null" class="mt-6 pt-6 border-t border-gray-200">
<h4 class="text-sm font-semibold text-gray-700 mb-3">Tulajdonosi státusz</h4>
<p class="text-sm text-gray-600 mb-4">Válaszd ki, hogy milyen minőségben vagy kapcsolatban a járművel:</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
@click="ownershipType = 'owner'"
class="p-4 rounded-lg border-2 transition-all duration-200 cursor-pointer"
:class="ownershipType === 'owner'
? (isFleetMode ? 'border-blue-500 bg-blue-50' : 'border-green-500 bg-green-50')
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50'"
>
<div class="flex items-center">
<div class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center mr-4"
:class="ownershipType === 'owner'
? (isFleetMode ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600')
: 'bg-gray-100 text-gray-500'">
<span>👑</span>
</div>
<div class="flex-grow">
<h4 class="font-medium text-gray-900">Tulajdonos vagyok</h4>
<p class="text-sm text-gray-600">A jármű a tulajdonom, én fizetem a költségeket és döntök a szervizelésről.</p>
</div>
<div v-if="ownershipType === 'owner'" class="text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
</div>
<div
@click="ownershipType = 'operator'"
class="p-4 rounded-lg border-2 transition-all duration-200 cursor-pointer"
:class="ownershipType === 'operator'
? (isFleetMode ? 'border-blue-500 bg-blue-50' : 'border-green-500 bg-green-50')
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50'"
>
<div class="flex items-center">
<div class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center mr-4"
:class="ownershipType === 'operator'
? (isFleetMode ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600')
: 'bg-gray-100 text-gray-500'">
<span>🚗</span>
</div>
<div class="flex-grow">
<h4 class="font-medium text-gray-900">Csak üzembentartó/használó vagyok</h4>
<p class="text-sm text-gray-600">A járművet használom, de nem én vagyok a tulajdonos (pl. céges autó, lízing).</p>
</div>
<div v-if="ownershipType === 'operator'" class="text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
</div>
</div>
<div class="mt-4 text-sm text-gray-600">
<p v-if="ownershipType === 'owner'">
<strong>Hatás:</strong> A kiválasztott szervezet lesz a tulajdonos (<code>owner_org_id</code>) és az üzembentartó (<code>operator_org_id</code>) is.
</p>
<p v-else>
<strong>Hatás:</strong> A kiválasztott szervezet lesz csak az üzembentartó (<code>operator_org_id</code>). A tulajdonos mező üres marad (<code>owner_org_id = null</code>).
</p>
</div>
</div>
</div>
<div v-if="loadingOrganizations" class="mt-4 text-center text-gray-500">
Szervezetek betöltése...
</div>
</div>
<!-- Step 1: Classification -->
<div v-else-if="currentStep === 1">
<h3 class="text-lg font-semibold mb-4 text-gray-800">Step {{ needsOrganizationSelection ? '2' : '1' }}: Vehicle Classification</h3>
<p class="text-sm mb-6 text-gray-600">
Select the type of vehicle you want to register. This helps us provide relevant catalog data.
</p>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<button
v-for="cls in vehicleClasses"
:key="cls.value"
@click="vehicleClass = cls.value"
class="p-4 rounded-lg border-2 transition-all duration-200 flex flex-col items-center justify-center"
:class="vehicleClass === cls.value
? (isFleetMode ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-green-500 bg-green-50 text-green-700')
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50'"
>
<span class="text-2xl mb-2">{{ cls.icon }}</span>
<span class="font-medium text-center">{{ cls.label }}</span>
</button>
</div>
</div>
<!-- Step 2: Catalog Auto-Complete -->
<div v-else-if="currentStep === 2">
<h3 class="text-lg font-semibold mb-4 text-gray-800">Step {{ needsOrganizationSelection ? '3' : '2' }}: Vehicle Catalog</h3>
<p class="text-sm mb-6 text-gray-600">
Select your vehicle from our catalog. Start by choosing the make, then model, generation, and engine.
</p>
<div class="space-y-6">
<!-- Make Selection -->
<div>
<label class="block text-sm font-medium mb-2 text-gray-700">Make (Brand)</label>
<select
v-model="make"
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
>
<option value="">Select a make</option>
<option v-for="m in makes" :key="m" :value="m">{{ m }}</option>
</select>
</div>
<!-- Model Selection -->
<div v-if="make">
<label class="block text-sm font-medium mb-2 text-gray-700">Model</label>
<select
v-model="model"
:disabled="!make || loadingModels"
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:opacity-50"
>
<option value="">Select a model</option>
<option v-for="m in models" :key="m" :value="m">{{ m }}</option>
</select>
<div v-if="loadingModels" class="text-sm text-gray-500 mt-1">Loading models...</div>
</div>
<!-- Generation Selection -->
<div v-if="model">
<label class="block text-sm font-medium mb-2 text-gray-700">Generation</label>
<select
v-model="generation"
:disabled="!model || loadingGenerations || !hasGenerations"
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:opacity-50"
>
<option value="">Select a generation</option>
<option v-for="g in generations" :key="g" :value="g">{{ g }}</option>
</select>
<div v-if="loadingGenerations" class="text-sm text-gray-500 mt-1">Loading generations...</div>
<div v-if="generationsLoaded && !hasGenerations" class="text-sm text-amber-600 mt-1 p-2 bg-amber-50 rounded border border-amber-200">
No generation data available for this model. You can proceed without selecting a generation.
</div>
</div>
<!-- Engine Selection -->
<div v-if="generation || (model && !hasGenerations)">
<label class="block text-sm font-medium mb-2 text-gray-700">Engine Variant</label>
<select
v-model="engine"
:disabled="(!generation && hasGenerations) || loadingEngines || !hasEngines"
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:opacity-50"
>
<option value="">Select an engine</option>
<option v-for="e in engines" :key="e.id || e.variant" :value="e.variant">{{ e.variant }} ({{ e.fuel_type }})</option>
</select>
<div v-if="loadingEngines" class="text-sm text-gray-500 mt-1">Loading engines...</div>
<div v-if="enginesLoaded && !hasEngines" class="text-sm text-amber-600 mt-1 p-2 bg-amber-50 rounded border border-amber-200">
No engine data available for this generation. You can proceed without selecting an engine.
</div>
<div v-if="catalogId" class="text-xs text-green-600 mt-1">Catalog ID: {{ catalogId }}</div>
</div>
</div>
</div>
<!-- Step 3: Unique Details -->
<div v-else-if="currentStep === 3">
<h3 class="text-lg font-semibold mb-4 text-gray-800">Step {{ needsOrganizationSelection ? '4' : '3' }}: Vehicle Details</h3>
<p class="text-sm mb-6 text-gray-600">
Provide the unique details for your vehicle.
</p>
<div class="space-y-6">
<!-- License Plate -->
<div>
<label class="block text-sm font-medium mb-2 text-gray-700">License Plate <span class="text-red-500">*</span></label>
<input
v-model="licensePlate"
type="text"
required
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="ABC-123"
/>
</div>
<!-- VIN (Optional) -->
<div>
<label class="block text-sm font-medium mb-2 text-gray-700">VIN (Opcionális)</label>
<input
v-model="vin"
type="text"
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="1HGCM82633A123456"
/>
<p class="text-xs text-gray-500 mt-1">Járműazonosító szám (17 karakter)</p>
<!-- Grace Period Warning -->
<div v-if="!vin" class="mt-3 p-3 rounded-lg bg-amber-50 border border-amber-200">
<div class="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-amber-600 mr-2 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
<div>
<p class="text-sm font-medium text-amber-800">Figyelem: Átmeneti használat</p>
<p class="text-xs text-amber-700 mt-1">
Az alvázszám (VIN) megadása nélkül a jármű <strong>14 napig</strong>, vagy maximum <strong>10 költség rögzítéséig</strong> használható.
Ezt követően a rendszer zárolja a további műveleteket, amíg meg nem adod az alvázszámot.
</p>
<p class="text-xs text-amber-700 mt-1">
Ajánlott az alvázszám megadása a teljes funkcionalitás érdekében.
</p>
</div>
</div>
</div>
</div>
<!-- Current Mileage -->
<div>
<label class="block text-sm font-medium mb-2 text-gray-700">Current Mileage (km)</label>
<input
v-model="currentMileage"
type="number"
min="0"
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="150000"
/>
</div>
<!-- Color -->
<div>
<label class="block text-sm font-medium mb-2 text-gray-700">Color (Optional)</label>
<input
v-model="color"
type="text"
class="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-800 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="Red, Black, Silver, etc."
/>
</div>
</div>
</div>
<!-- Error and Success Messages -->
<div v-if="error" class="mt-6 p-4 rounded-lg bg-red-50 border border-red-200">
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<span class="text-red-700 font-medium">Error: {{ error }}</span>
</div>
</div>
<div v-if="successMessage" class="mt-6 p-4 rounded-lg bg-green-50 border border-green-200">
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span class="text-green-700 font-medium">{{ successMessage }}</span>
</div>
</div>
</div>
<!-- Modal Footer -->
<div class="p-6 border-t border-gray-200 bg-gray-50">
<div class="flex justify-between">
<button
v-if="currentStep > 1"
@click="prevStep"
class="px-5 py-2.5 rounded-lg border border-gray-300 text-gray-700 font-medium hover:bg-gray-50 transition-colors"
>
Previous
</button>
<div v-else></div>
<div class="flex space-x-3">
<button
@click="closeModal"
class="px-5 py-2.5 rounded-lg border border-gray-300 text-gray-700 font-medium hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
v-if="currentStep < totalSteps"
@click="nextStep"
:disabled="!canProceedToNextStep"
class="px-5 py-2.5 rounded-lg font-medium transition-colors"
:class="isFleetMode
? (canProceedToNextStep ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-blue-300 text-white cursor-not-allowed')
: (canProceedToNextStep ? 'bg-green-600 text-white hover:bg-green-700' : 'bg-green-300 text-white cursor-not-allowed')"
>
Next
</button>
<button
v-else
@click="handleSubmit"
:disabled="isLoading || !licensePlate"
class="px-5 py-2.5 rounded-lg font-medium transition-colors"
:class="isFleetMode
? (!isLoading && licensePlate ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-blue-300 text-white cursor-not-allowed')
: (!isLoading && licensePlate ? 'bg-green-600 text-white hover:bg-green-700' : 'bg-green-300 text-white cursor-not-allowed')"
>
<span v-if="isLoading">
<svg class="animate-spin h-5 w-5 text-white inline-block mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Registering...
</span>
<span v-else>Register Vehicle</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* Custom scrollbar for modal body */
.max-h-\[60vh\]::-webkit-scrollbar {
width: 6px;
}
.max-h-\[60vh\]::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.max-h-\[60vh\]::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.max-h-\[60vh\]::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
</style>

View File

@@ -1,169 +0,0 @@
<template>
<div class="analytics-dashboard">
<!-- Header with Mode Toggle -->
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 p-6 bg-gradient-to-r from-gray-50 to-white rounded-2xl shadow-sm border border-gray-200">
<div>
<h1 class="text-3xl font-bold text-gray-900">Vehicle Analytics & TCO Dashboard</h1>
<p class="text-gray-600 mt-2">
{{ isPrivateGarage ? 'Personal driving insights and fun achievements' : 'Corporate fleet performance and cost optimization' }}
</p>
</div>
<div class="mt-4 md:mt-0">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<span class="mr-3 text-sm font-medium text-gray-700">View Mode:</span>
<div class="relative inline-block w-64">
<div class="bg-gray-100 rounded-xl p-1 flex">
<button
@click="setMode('private_garage')"
:class="[
'flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all duration-200',
isPrivateGarage
? 'bg-white shadow text-gray-900'
: 'text-gray-600 hover:text-gray-900'
]"
>
<div class="flex items-center justify-center">
<span class="mr-2">🎮</span>
<span>Fun Stats</span>
</div>
</button>
<button
@click="setMode('corporate_fleet')"
:class="[
'flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all duration-200',
isCorporateFleet
? 'bg-white shadow text-gray-900'
: 'text-gray-600 hover:text-gray-900'
]"
>
<div class="flex items-center justify-center">
<span class="mr-2">📊</span>
<span>Business BI</span>
</div>
</button>
</div>
</div>
</div>
<button
@click="toggleMode"
class="px-4 py-3 bg-gradient-to-r from-blue-500 to-indigo-600 text-white rounded-xl font-medium hover:from-blue-600 hover:to-indigo-700 transition-all duration-200 shadow-md hover:shadow-lg flex items-center"
>
<span class="mr-2">🔄</span>
Switch to {{ isPrivateGarage ? 'Business' : 'Fun' }} View
</button>
</div>
<div class="mt-4 text-sm text-gray-500 flex items-center">
<div class="w-3 h-3 rounded-full bg-green-500 mr-2"></div>
<span>Live data updated {{ lastUpdated }}</span>
<button @click="refreshData" class="ml-4 text-blue-600 hover:text-blue-800 flex items-center">
<span class="mr-1"></span>
Refresh
</button>
</div>
</div>
</div>
<!-- Mode Indicator -->
<div class="mb-6">
<div v-if="isPrivateGarage" class="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-blue-100 to-indigo-100 text-blue-800">
<span class="mr-2">🎯</span>
<span class="font-medium">Private Garage Mode</span>
<span class="ml-2 text-sm">Personal insights and achievements</span>
</div>
<div v-else class="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-green-100 to-emerald-100 text-green-800">
<span class="mr-2">🏢</span>
<span class="font-medium">Corporate Fleet Mode</span>
<span class="ml-2 text-sm">Business intelligence and TCO analysis</span>
</div>
</div>
<!-- Dynamic Component -->
<div class="mt-6">
<component :is="currentComponent" />
</div>
<!-- Footer Notes -->
<div class="mt-12 pt-6 border-t border-gray-200">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-gray-50 p-4 rounded-xl">
<h4 class="font-semibold text-gray-800 mb-2">📈 Data Sources</h4>
<p class="text-sm text-gray-600">Vehicle telemetry, fuel receipts, maintenance records, and insurance data aggregated in real-time.</p>
</div>
<div class="bg-gray-50 p-4 rounded-xl">
<h4 class="font-semibold text-gray-800 mb-2">🎯 Key Metrics</h4>
<p class="text-sm text-gray-600">TCO (Total Cost of Ownership), Cost per km, Fuel efficiency, Utilization rate, and Environmental impact.</p>
</div>
<div class="bg-gray-50 p-4 rounded-xl">
<h4 class="font-semibold text-gray-800 mb-2">🔄 Auto-Sync</h4>
<p class="text-sm text-gray-600">Data updates every 24 hours. Manual refresh available. Historical data retained for 36 months.</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, shallowRef } from 'vue'
import { useAppModeStore } from '@/stores/appModeStore'
import FunStats from './FunStats.vue'
import BusinessBI from './BusinessBI.vue'
const appModeStore = useAppModeStore()
const { mode, isPrivateGarage, isCorporateFleet, toggleMode, setMode } = appModeStore
const lastUpdated = ref('just now')
const isLoading = ref(false)
const currentComponent = shallowRef(FunStats)
// Watch mode changes and update component
import { watch } from 'vue'
watch(() => mode.value, (newMode) => {
if (newMode === 'private_garage') {
currentComponent.value = FunStats
} else {
currentComponent.value = BusinessBI
}
}, { immediate: true })
const refreshData = () => {
isLoading.value = true
// Simulate API call
setTimeout(() => {
const now = new Date()
lastUpdated.value = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
isLoading.value = false
// Show success notification
const event = new CustomEvent('show-toast', {
detail: {
message: 'Analytics data refreshed successfully',
type: 'success'
}
})
window.dispatchEvent(event)
}, 800)
}
</script>
<style scoped>
.analytics-dashboard {
font-family: 'Inter', sans-serif;
}
/* Smooth transitions for mode switching */
.component-enter-active,
.component-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.component-enter-from,
.component-leave-to {
opacity: 0;
transform: translateY(10px);
}
</style>

View File

@@ -1,385 +0,0 @@
<template>
<div class="business-bi">
<h2 class="text-2xl font-bold text-gray-800 mb-6">📊 Business Intelligence Dashboard</h2>
<!-- Key Metrics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Fleet Size</p>
<p class="text-3xl font-bold text-gray-800">{{ businessMetrics.fleetSize }}</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<span class="text-2xl text-blue-600">🚗</span>
</div>
</div>
<p class="text-sm text-gray-500 mt-2">Active vehicles</p>
</div>
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Total Monthly Cost</p>
<p class="text-3xl font-bold text-gray-800">{{ formatNumber(businessMetrics.totalMonthlyCost) }}</p>
</div>
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<span class="text-2xl text-green-600">💰</span>
</div>
</div>
<p class="text-sm text-gray-500 mt-2">All expenses combined</p>
</div>
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Avg Cost per Km</p>
<p class="text-3xl font-bold text-gray-800">{{ businessMetrics.averageCostPerKm.toFixed(2) }}</p>
</div>
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<span class="text-2xl text-purple-600">📈</span>
</div>
</div>
<p class="text-sm text-gray-500 mt-2">Operating efficiency</p>
</div>
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Utilization Rate</p>
<p class="text-3xl font-bold text-gray-800">{{ businessMetrics.utilizationRate }}%</p>
</div>
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center">
<span class="text-2xl text-amber-600"></span>
</div>
</div>
<p class="text-sm text-gray-500 mt-2">Fleet activity</p>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Monthly Costs Chart -->
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Monthly Cost Breakdown (Last 6 Months)</h3>
<div class="h-80">
<canvas ref="monthlyCostsChart"></canvas>
</div>
<div class="mt-4 text-sm text-gray-500">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<div class="w-3 h-3 bg-blue-500 rounded-full mr-2"></div>
<span>Maintenance</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
<span>Fuel</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 bg-amber-500 rounded-full mr-2"></div>
<span>Insurance</span>
</div>
</div>
</div>
</div>
<!-- Fuel Efficiency Trend Chart -->
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Fuel Efficiency Trend (km per liter)</h3>
<div class="h-80">
<canvas ref="fuelEfficiencyChart"></canvas>
</div>
<div class="mt-4 text-sm text-gray-500">
<p>Average: <span class="font-semibold">{{ averageFuelEfficiency.toFixed(1) }} km/L</span></p>
<p class="text-green-600"> {{ ((averageFuelEfficiency - 12) / 12 * 100).toFixed(1) }}% improvement vs industry average</p>
</div>
</div>
</div>
<!-- Cost per Km and TCO Analysis -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Cost per Km Chart -->
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Cost per Kilometer Trend</h3>
<div class="h-64">
<canvas ref="costPerKmChart"></canvas>
</div>
<div class="mt-4 text-sm text-gray-500">
<p>Average cost: <span class="font-semibold">{{ averageCostPerKm.toFixed(2) }}/km</span></p>
<p>Target: <span class="font-semibold">0.38/km</span></p>
</div>
</div>
<!-- TCO Breakdown -->
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Total Cost of Ownership (TCO) Breakdown</h3>
<div class="h-64">
<canvas ref="tcoChart"></canvas>
</div>
<div class="mt-4 text-sm text-gray-500">
<p>Annual TCO: <span class="font-semibold">{{ formatNumber(businessMetrics.totalMonthlyCost * 12) }}</span></p>
<p>Per vehicle: <span class="font-semibold">{{ formatNumber(Math.round(businessMetrics.totalMonthlyCost * 12 / businessMetrics.fleetSize)) }}/year</span></p>
</div>
</div>
</div>
<!-- Data Table -->
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Monthly Performance Details</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr class="bg-gray-50">
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Month</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Maintenance</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fuel</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Insurance</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cost/km</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Efficiency</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="(month, index) in monthlyCosts" :key="month.month">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ month.month }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ month.maintenance }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ month.fuel }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ month.insurance }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">{{ month.total }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ costPerKmTrends[index]?.cost.toFixed(2) || '0.00' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ fuelEfficiencyTrends[index]?.efficiency.toFixed(1) || '0.0' }} km/L</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Insights Panel -->
<div class="mt-8 bg-gradient-to-r from-gray-800 to-gray-900 rounded-2xl p-6 text-white shadow-lg">
<div class="flex items-center mb-4">
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center mr-4">
<span class="text-xl">💡</span>
</div>
<div>
<h3 class="text-xl font-bold">Business Insights</h3>
<p class="text-gray-300">AI-powered recommendations</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<div class="bg-white/10 p-4 rounded-xl">
<h4 class="font-semibold mb-2">💰 Cost Optimization</h4>
<p class="text-sm text-gray-200">Maintenance costs are {{ getCostComparison() }} than industry average. Consider preventive maintenance scheduling to reduce unexpected repairs.</p>
</div>
<div class="bg-white/10 p-4 rounded-xl">
<h4 class="font-semibold mb-2"> Fuel Efficiency</h4>
<p class="text-sm text-gray-200">Your fleet is {{ getEfficiencyComparison() }} efficient than benchmark. Continue driver training programs for optimal performance.</p>
</div>
<div class="bg-white/10 p-4 rounded-xl">
<h4 class="font-semibold mb-2">📅 Utilization Rate</h4>
<p class="text-sm text-gray-200">{{ businessMetrics.utilizationRate }}% utilization is good. Consider dynamic routing to increase to 85% target.</p>
</div>
<div class="bg-white/10 p-4 rounded-xl">
<h4 class="font-semibold mb-2">🔧 Downtime Management</h4>
<p class="text-sm text-gray-200">{{ businessMetrics.downtimeHours }} hours/month downtime. Predictive maintenance could reduce this by 30%.</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useAnalyticsStore } from '@/stores/analyticsStore'
import { storeToRefs } from 'pinia'
import { Chart, registerables } from 'chart.js'
Chart.register(...registerables)
const analyticsStore = useAnalyticsStore()
const { monthlyCosts, fuelEfficiencyTrends, costPerKmTrends, businessMetrics, averageFuelEfficiency, averageCostPerKm } = storeToRefs(analyticsStore)
const monthlyCostsChart = ref(null)
const fuelEfficiencyChart = ref(null)
const costPerKmChart = ref(null)
const tcoChart = ref(null)
let chartInstances = []
const formatNumber = (num) => {
return new Intl.NumberFormat('en-US').format(num)
}
const getCostComparison = () => {
const avgMaintenance = monthlyCosts.value.reduce((sum, month) => sum + month.maintenance, 0) / monthlyCosts.value.length
return avgMaintenance > 500 ? 'higher' : avgMaintenance < 400 ? 'lower' : 'similar'
}
const getEfficiencyComparison = () => {
return averageFuelEfficiency.value > 13 ? 'more' : averageFuelEfficiency.value < 12 ? 'less' : 'equally'
}
onMounted(() => {
// Monthly Costs Chart (Stacked Bar)
if (monthlyCostsChart.value) {
const ctx = monthlyCostsChart.value.getContext('2d')
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: monthlyCosts.value.map(m => m.month),
datasets: [
{
label: 'Maintenance',
data: monthlyCosts.value.map(m => m.maintenance),
backgroundColor: '#3b82f6',
stack: 'Stack 0',
},
{
label: 'Fuel',
data: monthlyCosts.value.map(m => m.fuel),
backgroundColor: '#10b981',
stack: 'Stack 0',
},
{
label: 'Insurance',
data: monthlyCosts.value.map(m => m.insurance),
backgroundColor: '#f59e0b',
stack: 'Stack 0',
},
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Cost (€)'
}
}
}
}
})
chartInstances.push(chart)
}
// Fuel Efficiency Chart (Line)
if (fuelEfficiencyChart.value) {
const ctx = fuelEfficiencyChart.value.getContext('2d')
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: fuelEfficiencyTrends.value.map(m => m.month),
datasets: [
{
label: 'Fuel Efficiency (km/L)',
data: fuelEfficiencyTrends.value.map(m => m.efficiency),
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.1)',
fill: true,
tension: 0.4,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
title: {
display: true,
text: 'km per liter'
}
}
}
}
})
chartInstances.push(chart)
}
// Cost per Km Chart (Line)
if (costPerKmChart.value) {
const ctx = costPerKmChart.value.getContext('2d')
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: costPerKmTrends.value.map(m => m.month),
datasets: [
{
label: 'Cost per Kilometer (€)',
data: costPerKmTrends.value.map(m => m.cost),
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
fill: true,
tension: 0.4,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
title: {
display: true,
text: '€ per km'
}
}
}
}
})
chartInstances.push(chart)
}
// TCO Chart (Doughnut)
if (tcoChart.value) {
const ctx = tcoChart.value.getContext('2d')
const totalMaintenance = monthlyCosts.value.reduce((sum, month) => sum + month.maintenance, 0)
const totalFuel = monthlyCosts.value.reduce((sum, month) => sum + month.fuel, 0)
const totalInsurance = monthlyCosts.value.reduce((sum, month) => sum + month.insurance, 0)
const chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Maintenance', 'Fuel', 'Insurance'],
datasets: [
{
data: [totalMaintenance, totalFuel, totalInsurance],
backgroundColor: ['#3b82f6', '#10b981', '#f59e0b'],
borderWidth: 2,
borderColor: '#ffffff',
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
}
}
}
})
chartInstances.push(chart)
}
})
onUnmounted(() => {
chartInstances.forEach(chart => chart.destroy())
chartInstances = []
})
</script>
<style scoped>
.business-bi {
font-family: 'Inter', sans-serif;
}
</style>

View File

@@ -1,168 +0,0 @@
<template>
<div class="fun-stats">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🎮 Fun Stats & Achievements</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Moon Trip Card -->
<div class="bg-gradient-to-br from-blue-50 to-indigo-100 rounded-2xl p-6 shadow-lg border border-blue-200">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mr-4">
<span class="text-2xl">🌙</span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800">Moon Trip</h3>
<p class="text-sm text-gray-600">Distance traveled</p>
</div>
</div>
<div class="text-center py-4">
<div class="text-4xl font-bold text-blue-700">{{ funFacts.moonTrips }}</div>
<p class="text-gray-600 mt-2">trips to the Moon</p>
</div>
<p class="text-sm text-gray-500 text-center">
You've driven <span class="font-semibold">{{ formatNumber(funFacts.totalKmDriven) }} km</span> - that's {{ funFacts.moonTrips }} trip{{ funFacts.moonTrips !== 1 ? 's' : '' }} to the Moon!
</p>
</div>
<!-- Earth Circuits Card -->
<div class="bg-gradient-to-br from-green-50 to-emerald-100 rounded-2xl p-6 shadow-lg border border-green-200">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mr-4">
<span class="text-2xl">🌍</span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800">Earth Circuits</h3>
<p class="text-sm text-gray-600">Around the world</p>
</div>
</div>
<div class="text-center py-4">
<div class="text-4xl font-bold text-green-700">{{ funFacts.earthCircuits }}</div>
<p class="text-gray-600 mt-2">times around Earth</p>
</div>
<p class="text-sm text-gray-500 text-center">
Equivalent to {{ funFacts.earthCircuits }} circuit{{ funFacts.earthCircuits !== 1 ? 's' : '' }} around the equator!
</p>
</div>
<!-- Trees Saved Card -->
<div class="bg-gradient-to-br from-amber-50 to-orange-100 rounded-2xl p-6 shadow-lg border border-amber-200">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center mr-4">
<span class="text-2xl">🌳</span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800">Trees Saved</h3>
<p class="text-sm text-gray-600">Environmental impact</p>
</div>
</div>
<div class="text-center py-4">
<div class="text-4xl font-bold text-amber-700">{{ funFacts.totalTreesSaved }}</div>
<p class="text-gray-600 mt-2">trees preserved</p>
</div>
<p class="text-sm text-gray-500 text-center">
Your efficient driving saved {{ funFacts.totalTreesSaved }} tree{{ funFacts.totalTreesSaved !== 1 ? 's' : '' }} from CO₂ emissions!
</p>
</div>
<!-- CO₂ Saved Card -->
<div class="bg-gradient-to-br from-purple-50 to-pink-100 rounded-2xl p-6 shadow-lg border border-purple-200">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mr-4">
<span class="text-2xl"></span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800">CO₂ Saved</h3>
<p class="text-sm text-gray-600">Carbon footprint</p>
</div>
</div>
<div class="text-center py-4">
<div class="text-4xl font-bold text-purple-700">{{ funFacts.totalCo2Saved }}</div>
<p class="text-gray-600 mt-2">tons of CO₂</p>
</div>
<p class="text-sm text-gray-500 text-center">
That's like taking {{ Math.round(funFacts.totalCo2Saved * 1.8) }} cars off the road for a year!
</p>
</div>
<!-- Money Saved Card -->
<div class="bg-gradient-to-br from-cyan-50 to-teal-100 rounded-2xl p-6 shadow-lg border border-cyan-200">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-cyan-100 rounded-xl flex items-center justify-center mr-4">
<span class="text-2xl">💰</span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800">Money Saved</h3>
<p class="text-sm text-gray-600">Smart driving pays off</p>
</div>
</div>
<div class="text-center py-4">
<div class="text-4xl font-bold text-cyan-700">€{{ formatNumber(funFacts.totalMoneySaved) }}</div>
<p class="text-gray-600 mt-2">total savings</p>
</div>
<p class="text-sm text-gray-500 text-center">
Compared to average drivers, you saved €{{ formatNumber(funFacts.totalMoneySaved) }}!
</p>
</div>
<!-- Fuel Efficiency Card -->
<div class="bg-gradient-to-br from-rose-50 to-red-100 rounded-2xl p-6 shadow-lg border border-rose-200">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-rose-100 rounded-xl flex items-center justify-center mr-4">
<span class="text-2xl">⛽</span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800">Fuel Efficiency</h3>
<p class="text-sm text-gray-600">Your average</p>
</div>
</div>
<div class="text-center py-4">
<div class="text-4xl font-bold text-rose-700">{{ averageFuelEfficiency.toFixed(1) }}</div>
<p class="text-gray-600 mt-2">km per liter</p>
</div>
<p class="text-sm text-gray-500 text-center">
{{ getEfficiencyMessage(averageFuelEfficiency) }}
</p>
</div>
</div>
<!-- Fun Fact of the Day -->
<div class="mt-8 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-2xl p-6 text-white shadow-lg">
<div class="flex items-center">
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center mr-4">
<span class="text-xl">💡</span>
</div>
<div>
<h3 class="text-xl font-bold">Fun Fact of the Day</h3>
<p class="text-indigo-100">Did you know?</p>
</div>
</div>
<p class="mt-4 text-lg">
If every driver in your city achieved your fuel efficiency, we'd save enough CO₂ to fill {{ Math.round(funFacts.totalCo2Saved * 100) }} hot air balloons every year!
</p>
</div>
</div>
</template>
<script setup>
import { useAnalyticsStore } from '@/stores/analyticsStore'
import { storeToRefs } from 'pinia'
const analyticsStore = useAnalyticsStore()
const { funFacts, averageFuelEfficiency } = storeToRefs(analyticsStore)
const formatNumber = (num) => {
return new Intl.NumberFormat('en-US').format(num)
}
const getEfficiencyMessage = (efficiency) => {
if (efficiency > 15) return "Outstanding! You're among the top 5% most efficient drivers."
if (efficiency > 12) return "Great job! You're more efficient than 80% of drivers."
if (efficiency > 10) return "Good! You're above average in fuel efficiency."
return 'Room for improvement. Check our tips to save more fuel.'
}
</script>
<style scoped>
.fun-stats {
font-family: 'Inter', sans-serif;
}
</style>

View File

@@ -1,141 +0,0 @@
<template>
<div class="achievement-showcase">
<!-- Mode indicator -->
<div class="mode-indicator mb-8 p-4 rounded-xl bg-gradient-to-r from-slate-50 to-gray-100 border border-gray-200">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div
class="w-10 h-10 rounded-full flex items-center justify-center mr-4"
:class="isPrivateGarage ? 'bg-amber-100 text-amber-700' : 'bg-emerald-100 text-emerald-700'"
>
{{ isPrivateGarage ? '🏆' : '🏅' }}
</div>
<div>
<h3 class="font-bold text-lg">
{{ isPrivateGarage ? 'Private Garage Trophy Showcase' : 'Corporate Fleet Badge Board' }}
</h3>
<p class="text-sm text-gray-600">
{{ isPrivateGarage
? 'Playful trophies for personal achievements'
: 'Professional badges for fleet optimization'
}}
</p>
</div>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-gray-500">
Current mode:
<span class="font-semibold" :class="isPrivateGarage ? 'text-amber-700' : 'text-emerald-700'">
{{ isPrivateGarage ? 'Private Garage' : 'Corporate Fleet' }}
</span>
</div>
<button
@click="toggleMode"
class="px-4 py-2 text-sm font-medium rounded-lg border transition-colors"
:class="isPrivateGarage
? 'border-amber-300 text-amber-700 bg-amber-50 hover:bg-amber-100'
: 'border-emerald-300 text-emerald-700 bg-emerald-50 hover:bg-emerald-100'"
>
Switch to {{ isPrivateGarage ? 'Corporate' : 'Private' }}
</button>
</div>
</div>
</div>
<!-- Dynamic component rendering -->
<div class="component-container">
<TrophyCabinet v-if="isPrivateGarage" />
<BadgeBoard v-else />
</div>
<!-- Gamification stats -->
<div class="mt-10 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="p-5 rounded-xl bg-white border border-gray-200 shadow-sm">
<div class="flex items-center">
<div class="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 text-xl mr-4">
📈
</div>
<div>
<div class="text-2xl font-bold text-gray-900">{{ earnedCount }}</div>
<div class="text-sm text-gray-600">Achievements Earned</div>
</div>
</div>
</div>
<div class="p-5 rounded-xl bg-white border border-gray-200 shadow-sm">
<div class="flex items-center">
<div class="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center text-purple-600 text-xl mr-4">
🎯
</div>
<div>
<div class="text-2xl font-bold text-gray-900">{{ progressPercentage }}%</div>
<div class="text-sm text-gray-600">Overall Progress</div>
</div>
</div>
</div>
<div class="p-5 rounded-xl bg-white border border-gray-200 shadow-sm">
<div class="flex items-center">
<div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center text-green-600 text-xl mr-4">
</div>
<div>
<div class="text-2xl font-bold text-gray-900">{{ nextAchievement }}</div>
<div class="text-sm text-gray-600">Next Achievement</div>
</div>
</div>
</div>
</div>
<!-- Help text -->
<div class="mt-8 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div class="flex items-start">
<div class="text-gray-500 mr-3">💡</div>
<div class="text-sm text-gray-600">
<span class="font-semibold">How to earn more:</span>
{{ isPrivateGarage
? 'Add vehicles, log expenses, complete daily quizzes, and find services to unlock trophies.'
: 'Optimize fleet efficiency, reduce costs, manage multiple vehicles, and maintain service records to earn badges.'
}}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useAppModeStore } from '@/stores/appModeStore'
import { useGamificationStore } from '@/stores/gamificationStore'
import { storeToRefs } from 'pinia'
import TrophyCabinet from './TrophyCabinet.vue'
import BadgeBoard from './BadgeBoard.vue'
const appModeStore = useAppModeStore()
const gamificationStore = useGamificationStore()
const { isPrivateGarage, isCorporateFleet, toggleMode } = appModeStore
const { earnedCount, progressPercentage, lockedAchievements } = storeToRefs(gamificationStore)
const nextAchievement = computed(() => {
if (lockedAchievements.value.length > 0) {
return lockedAchievements.value[0].title
}
return 'All earned!'
})
</script>
<style scoped>
.achievement-showcase {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -1,193 +0,0 @@
<template>
<div class="badge-board">
<div class="mb-8">
<h2 class="text-2xl font-bold text-slate-800 mb-2">🏅 Efficiency Badges</h2>
<p class="text-gray-600">Professional recognition for fleet optimization and cost management.</p>
<div class="mt-6 flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-emerald-500 mr-2"></div>
<span class="text-sm text-gray-700">Earned: {{ earnedCount }}</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-gray-300 mr-2"></div>
<span class="text-sm text-gray-700">Available: {{ totalAchievements - earnedCount }}</span>
</div>
</div>
<div class="text-right">
<div class="text-sm text-gray-500">Fleet Score</div>
<div class="text-2xl font-bold text-slate-800">{{ fleetScore }}/100</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="achievement in achievements"
:key="achievement.id"
class="badge-card p-6 rounded-xl border transition-all duration-300"
:class="[
achievement.isEarned
? 'border-emerald-200 bg-white shadow-md hover:shadow-lg'
: 'border-gray-200 bg-gray-50'
]"
>
<!-- Badge header -->
<div class="flex items-start justify-between mb-4">
<div class="flex items-center">
<div
class="w-12 h-12 rounded-full flex items-center justify-center text-2xl"
:class="achievement.isEarned ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-200 text-gray-400'"
>
{{ achievement.icon }}
</div>
<div class="ml-4">
<h3 class="font-bold text-lg" :class="achievement.isEarned ? 'text-slate-900' : 'text-gray-500'">
{{ achievement.title }}
</h3>
<div class="text-xs font-medium px-2 py-1 rounded-full inline-block mt-1"
:class="achievement.isEarned ? 'bg-blue-100 text-blue-700' : 'bg-gray-200 text-gray-500'">
{{ achievement.category.toUpperCase() }}
</div>
</div>
</div>
<!-- Status indicator -->
<div v-if="achievement.isEarned" class="text-emerald-600">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</div>
<div v-else class="text-gray-400">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
</div>
<!-- Description -->
<p class="text-gray-600 mb-5" :class="{ 'opacity-70': !achievement.isEarned }">
{{ achievement.description }}
</p>
<!-- Progress bar for unearned badges -->
<div v-if="!achievement.isEarned" class="mt-4">
<div class="flex justify-between text-xs text-gray-500 mb-1">
<span>Progress</span>
<span>0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-gray-400 h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
<!-- Earned details -->
<div v-if="achievement.isEarned" class="mt-4 pt-4 border-t border-gray-100">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
<span class="font-medium">Awarded:</span> {{ achievement.earnedDate }}
</div>
<div class="text-sm font-semibold text-emerald-700">
+{{ badgePoints(achievement.category) }} pts
</div>
</div>
</div>
<!-- Action button -->
<div class="mt-6">
<button
v-if="!achievement.isEarned"
class="w-full py-2 text-sm font-medium rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
disabled
>
Not Yet Achieved
</button>
<button
v-else
class="w-full py-2 text-sm font-medium rounded-lg bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition-colors"
>
View Details
</button>
</div>
</div>
</div>
<!-- Summary stats -->
<div class="mt-10 p-6 bg-slate-50 rounded-xl border border-slate-200">
<h3 class="font-bold text-lg text-slate-800 mb-4">Fleet Performance Summary</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<div class="text-center">
<div class="text-3xl font-bold text-slate-800">{{ earnedCount }}</div>
<div class="text-sm text-gray-600">Badges Earned</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-slate-800">{{ fleetScore }}</div>
<div class="text-sm text-gray-600">Fleet Score</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-slate-800">{{ efficiencyBadgesCount }}</div>
<div class="text-sm text-gray-600">Efficiency Badges</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-slate-800">{{ corporateBadgesCount }}</div>
<div class="text-sm text-gray-600">Corporate Badges</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useGamificationStore } from '@/stores/gamificationStore'
import { storeToRefs } from 'pinia'
const gamificationStore = useGamificationStore()
const {
achievements,
earnedCount,
totalAchievements
} = storeToRefs(gamificationStore)
// Computed
const fleetScore = computed(() => {
const base = earnedCount.value * 12
return Math.min(base, 100)
})
const efficiencyBadgesCount = computed(() => {
return achievements.value.filter(a =>
a.category === 'efficiency' && a.isEarned
).length
})
const corporateBadgesCount = computed(() => {
return achievements.value.filter(a =>
a.category === 'corporate' && a.isEarned
).length
})
const badgePoints = (category) => {
const points = {
efficiency: 25,
corporate: 30,
finance: 20,
service: 15,
onboarding: 10,
knowledge: 15,
consistency: 10,
social: 5
}
return points[category] || 10
}
</script>
<style scoped>
.badge-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.badge-card:hover {
transform: translateY(-2px);
}
</style>

View File

@@ -1,108 +0,0 @@
<template>
<div class="trophy-cabinet">
<div class="mb-6">
<h2 class="text-2xl font-bold text-amber-800 mb-2">🏆 Trophy Cabinet</h2>
<p class="text-gray-600">Your earned achievements shine here! Collect more to fill your shelf.</p>
<div class="mt-4 flex items-center">
<div class="w-full bg-gray-200 rounded-full h-3">
<div
class="bg-gradient-to-r from-amber-400 to-amber-600 h-3 rounded-full transition-all duration-500"
:style="{ width: `${progressPercentage}%` }"
></div>
</div>
<span class="ml-4 text-sm font-semibold text-amber-700">{{ earnedCount }}/{{ totalAchievements }} ({{ progressPercentage }}%)</span>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<div
v-for="achievement in achievements"
:key="achievement.id"
class="relative group"
>
<div
class="trophy-card p-5 rounded-2xl border-2 transition-all duration-300 transform"
:class="[
achievement.isEarned
? 'border-amber-300 bg-gradient-to-br from-amber-50 to-amber-100 shadow-lg hover:shadow-2xl hover:scale-105'
: 'border-gray-300 bg-gray-100 opacity-60 grayscale'
]"
>
<!-- Trophy Icon -->
<div class="text-5xl mb-4 text-center">
{{ achievement.icon }}
</div>
<!-- Lock overlay for unearned -->
<div
v-if="!achievement.isEarned"
class="absolute inset-0 bg-gray-800 bg-opacity-70 rounded-2xl flex items-center justify-center"
>
<div class="text-white text-center">
<div class="text-3xl mb-2">🔒</div>
<div class="text-sm font-semibold">Locked</div>
</div>
</div>
<!-- Content -->
<h3 class="text-lg font-bold mb-2" :class="achievement.isEarned ? 'text-gray-900' : 'text-gray-500'">
{{ achievement.title }}
</h3>
<p class="text-sm mb-3" :class="achievement.isEarned ? 'text-gray-700' : 'text-gray-400'">
{{ achievement.description }}
</p>
<!-- Category badge -->
<div class="inline-block px-3 py-1 text-xs rounded-full"
:class="achievement.isEarned ? 'bg-amber-200 text-amber-800' : 'bg-gray-300 text-gray-500'">
{{ achievement.category }}
</div>
<!-- Earned date -->
<div v-if="achievement.isEarned" class="mt-4 pt-3 border-t border-amber-200">
<div class="text-xs text-amber-600 font-medium">
🎉 Earned on {{ achievement.earnedDate }}
</div>
</div>
</div>
<!-- Glow effect for earned trophies on hover -->
<div
v-if="achievement.isEarned"
class="absolute -inset-1 bg-gradient-to-r from-blue-400 to-blue-600 rounded-2xl blur opacity-0 group-hover:opacity-30 transition-opacity duration-300 -z-10"
></div>
</div>
</div>
<!-- Empty state message -->
<div v-if="earnedCount === 0" class="text-center py-12">
<div class="text-6xl mb-4">📭</div>
<h3 class="text-xl font-semibold text-gray-700 mb-2">No trophies yet!</h3>
<p class="text-gray-500">Start using the app to earn your first achievements.</p>
</div>
</div>
</template>
<script setup>
import { useGamificationStore } from '@/stores/gamificationStore'
import { storeToRefs } from 'pinia'
const gamificationStore = useGamificationStore()
const {
achievements,
earnedCount,
totalAchievements,
progressPercentage
} = storeToRefs(gamificationStore)
</script>
<style scoped>
.trophy-card {
position: relative;
z-index: 1;
min-height: 220px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>

View File

@@ -1,309 +0,0 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
vehicles: {
type: Array,
required: true,
default: () => []
}
})
// Enhanced status colors for corporate look
const statusColors = {
'OK': 'bg-emerald-50 text-emerald-700 border border-emerald-200',
'Service Due': 'bg-blue-50 text-blue-900 border border-blue-200 animate-pulse',
'Warning': 'bg-rose-50 text-rose-700 border border-rose-200',
'draft': 'bg-yellow-50 text-yellow-800 border border-yellow-300',
'verified': 'bg-green-50 text-green-800 border border-green-300',
'active': 'bg-blue-50 text-blue-800 border border-blue-300',
'pending': 'bg-gray-50 text-gray-800 border border-gray-300',
'incomplete': 'bg-orange-50 text-orange-800 border border-orange-300'
}
const sortedVehicles = computed(() => {
return [...props.vehicles].sort((a, b) => b.monthlyExpense - a.monthlyExpense)
})
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(amount)
}
const formatMileage = (mileage) => {
return new Intl.NumberFormat('en-US').format(mileage)
}
// Country flag mapping
const getCountryFlag = (make) => {
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')) {
return 'https://flagcdn.com/w40/us.png'
} else if (makeLower.includes('toyota') || makeLower.includes('honda') || makeLower.includes('nissan') || makeLower.includes('mazda')) {
return 'https://flagcdn.com/w40/jp.png'
} else if (makeLower.includes('ferrari') || makeLower.includes('lamborghini') || makeLower.includes('fiat') || makeLower.includes('alfa romeo')) {
return 'https://flagcdn.com/w40/it.png'
} else if (makeLower.includes('volvo') || makeLower.includes('saab')) {
return 'https://flagcdn.com/w40/se.png'
} else if (makeLower.includes('renault') || makeLower.includes('peugeot') || makeLower.includes('citroen')) {
return 'https://flagcdn.com/w40/fr.png'
} else if (makeLower.includes('skoda') || makeLower.includes('seat')) {
return 'https://flagcdn.com/w40/cz.png'
} else {
return 'https://flagcdn.com/w40/eu.png'
}
}
</script>
<template>
<div class="bg-white/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-gray-300/50 overflow-hidden">
<!-- Corporate Glass Header -->
<div class="px-8 py-5 border-b border-gray-300/30 bg-gradient-to-r from-slate-900/90 to-slate-800/90 backdrop-blur-md">
<div class="flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-white tracking-tight">Corporate Fleet Management</h2>
<p class="text-sm text-slate-300 mt-1">Enterprise-grade vehicle oversight with real-time analytics</p>
</div>
<div class="text-right">
<div class="text-3xl font-bold text-white">{{ formatCurrency(vehicles.reduce((sum, v) => sum + v.monthlyExpense, 0)) }}</div>
<div class="text-sm text-slate-300">Total monthly fleet cost {{ vehicles.length }} assets</div>
</div>
</div>
</div>
<!-- Table Container -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300/30">
<thead class="bg-gradient-to-r from-slate-100 to-slate-200/80">
<tr>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2">🚗</span> Vehicle
</div>
</th>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2">🏷</span> License Plate
</div>
</th>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2">📅</span> Year
</div>
</th>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2">📊</span> Mileage
</div>
</th>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2"></span> Fuel Type
</div>
</th>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2">🔧</span> Data Status
</div>
</th>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2">💰</span> Monthly Cost
</div>
</th>
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
<div class="flex items-center">
<span class="mr-2"></span> Actions
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-300/20">
<tr
v-for="(vehicle, index) in sortedVehicles"
:key="vehicle.id"
:class="[
'transition-all duration-200 hover:bg-slate-100/80',
index % 2 === 0 ? 'bg-white' : 'bg-slate-50/70'
]"
>
<td class="px-8 py-5 whitespace-nowrap">
<div class="flex items-center">
<div class="h-12 w-12 flex-shrink-0 bg-gradient-to-br from-slate-200 to-slate-300 rounded-xl overflow-hidden mr-4 shadow-sm border border-slate-300/50">
<img
:src="vehicle.imageUrl"
:alt="`${vehicle.make} ${vehicle.model}`"
class="h-full w-full object-cover"
/>
</div>
<div>
<div class="font-bold text-slate-900 text-lg">{{ vehicle.make }} {{ vehicle.model }}</div>
<div class="text-sm text-slate-600 mt-1">ID: {{ vehicle.id }} Asset</div>
</div>
</div>
</td>
<td class="px-8 py-5 whitespace-nowrap">
<div class="flex items-center space-x-3">
<img
:src="getCountryFlag(vehicle.make)"
:alt="`${vehicle.make} origin flag`"
class="w-6 h-4 rounded-sm shadow-md border border-slate-300"
/>
<div>
<div class="font-mono font-bold text-slate-900 text-lg tracking-wider">{{ vehicle.licensePlate }}</div>
<div class="text-xs text-slate-500 mt-1">Registered</div>
</div>
</div>
</td>
<td class="px-8 py-5 whitespace-nowrap">
<div class="text-center">
<div class="text-2xl font-bold text-slate-900">{{ vehicle.year }}</div>
<div class="text-xs text-slate-500 mt-1">Model Year</div>
</div>
</td>
<td class="px-8 py-5 whitespace-nowrap">
<div class="text-center">
<div class="text-2xl font-bold text-slate-900">{{ formatMileage(vehicle.mileage) }}</div>
<div class="text-xs text-slate-500 mt-1">km</div>
</div>
</td>
<td class="px-8 py-5 whitespace-nowrap">
<span :class="[
'px-4 py-2 rounded-full text-sm font-semibold shadow-sm',
vehicle.fuelType === 'Electric' ? 'bg-emerald-100 text-emerald-800 border border-emerald-300' :
vehicle.fuelType === 'Diesel' ? 'bg-blue-100 text-blue-800 border border-blue-300' :
'bg-amber-100 text-amber-800 border border-amber-300'
]">
{{ vehicle.fuelType }}
</span>
</td>
<td class="px-8 py-5 whitespace-nowrap">
<div class="space-y-2">
<!-- Data Status Badge -->
<div class="flex items-center">
<span
:class="['px-3 py-1.5 rounded-full text-xs font-semibold flex items-center', statusColors[vehicle.data_status || vehicle.status] || 'bg-slate-100 text-slate-800 border border-slate-300']"
>
<span
class="w-2 h-2 rounded-full mr-2"
:class="{
'bg-emerald-500': (vehicle.data_status || vehicle.status) === 'OK' || (vehicle.data_status || vehicle.status) === 'verified',
'bg-blue-500': (vehicle.data_status || vehicle.status) === 'Service Due' || (vehicle.data_status || vehicle.status) === 'active',
'bg-rose-500': (vehicle.data_status || vehicle.status) === 'Warning',
'bg-yellow-500': (vehicle.data_status || vehicle.status) === 'draft',
'bg-gray-500': (vehicle.data_status || vehicle.status) === 'pending',
'bg-orange-500': (vehicle.data_status || vehicle.status) === 'incomplete'
}"
></span>
{{ vehicle.data_status || vehicle.status }}
</span>
</div>
<!-- Profile Completion Mini Progress Bar -->
<div v-if="vehicle.profile_completion_percentage !== undefined" class="flex items-center space-x-2">
<div class="w-16 bg-gray-200 rounded-full h-1.5">
<div
class="h-1.5 rounded-full transition-all duration-300"
:class="{
'bg-yellow-500': (vehicle.profile_completion_percentage || 0) < 50,
'bg-blue-500': (vehicle.profile_completion_percentage || 0) >= 50 && (vehicle.profile_completion_percentage || 0) < 80,
'bg-green-500': (vehicle.profile_completion_percentage || 0) >= 80
}"
:style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"
></div>
</div>
<span class="text-xs text-gray-600 font-medium">{{ vehicle.profile_completion_percentage || 0 }}%</span>
</div>
</div>
</td>
<td class="px-8 py-5 whitespace-nowrap">
<div class="text-center">
<div class="text-2xl font-bold text-slate-900">{{ formatCurrency(vehicle.monthlyExpense) }}</div>
<div class="text-xs text-slate-500 mt-1">per month</div>
</div>
</td>
<td class="px-8 py-5 whitespace-nowrap">
<div class="flex space-x-2">
<button class="px-4 py-2.5 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg">
View Details
</button>
<button class="px-3 py-2.5 border border-slate-300 hover:bg-slate-100 rounded-xl text-slate-700 transition-all duration-200 active:scale-95 shadow-sm hover:shadow">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Corporate Footer -->
<div class="px-8 py-5 border-t border-gray-300/30 bg-gradient-to-r from-slate-100 to-slate-200/80">
<div class="flex justify-between items-center">
<div class="text-sm text-slate-700">
<span class="font-semibold">Showing {{ vehicles.length }} of {{ vehicles.length }} corporate assets</span>
<span class="mx-2"></span>
<span>Last updated: {{ new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) }}</span>
</div>
<div class="flex space-x-3">
<button class="px-5 py-2.5 border border-slate-300 hover:bg-white text-slate-700 font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-sm hover:shadow flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Export CSV
</button>
<button class="px-5 py-2.5 bg-gradient-to-r from-emerald-600 to-emerald-700 hover:from-emerald-700 hover:to-emerald-800 text-white font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Vehicle
</button>
<button class="px-5 py-2.5 bg-gradient-to-r from-slate-700 to-slate-800 hover:from-slate-800 hover:to-slate-900 text-white font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Analytics
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* Custom table styles */
table {
border-spacing: 0;
}
/* Smooth row transitions */
tr {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Custom scrollbar for table */
.overflow-x-auto::-webkit-scrollbar {
height: 8px;
}
.overflow-x-auto::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
.overflow-x-auto::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>

View File

@@ -1,163 +0,0 @@
<script setup>
import { useThemeStore } from '@/stores/themeStore'
defineProps({
vehicle: {
type: Object,
required: true
}
})
const themeStore = useThemeStore()
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',
'draft': 'bg-yellow-100 text-yellow-800',
'verified': 'bg-green-100 text-green-800',
'active': 'bg-blue-100 text-blue-800',
'pending': 'bg-gray-100 text-gray-800',
'incomplete': 'bg-orange-100 text-orange-800'
}
const brandLogoUrl = (make) => {
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()
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')) {
return 'https://flagcdn.com/w40/us.png'
} else if (makeLower.includes('toyota') || makeLower.includes('honda') || makeLower.includes('nissan') || makeLower.includes('mazda')) {
return 'https://flagcdn.com/w40/jp.png'
} else if (makeLower.includes('ferrari') || makeLower.includes('lamborghini') || makeLower.includes('fiat') || makeLower.includes('alfa romeo')) {
return 'https://flagcdn.com/w40/it.png'
} else if (makeLower.includes('volvo') || makeLower.includes('saab')) {
return 'https://flagcdn.com/w40/se.png'
} else if (makeLower.includes('renault') || makeLower.includes('peugeot') || makeLower.includes('citroen')) {
return 'https://flagcdn.com/w40/fr.png'
} else if (makeLower.includes('skoda') || makeLower.includes('seat')) {
return 'https://flagcdn.com/w40/cz.png'
} else {
return 'https://flagcdn.com/w40/eu.png'
}
}
</script>
<template>
<div :class="['rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-500 border', themeClasses.card]">
<!-- Vehicle Image -->
<div class="h-48 bg-gray-200 relative overflow-hidden">
<img
:src="vehicle.imageUrl"
:alt="`${vehicle.make} ${vehicle.model}`"
class="w-full h-full object-cover"
/>
<!-- Brand Logo -->
<div class="absolute top-3 left-3 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-md">
<img
:src="brandLogoUrl(vehicle.make)"
:alt="vehicle.make"
class="w-8 h-8"
@error="(e) => e.target.style.display = 'none'"
/>
</div>
<div class="absolute top-3 right-3">
<span
:class="['px-3 py-1 rounded-full text-xs font-semibold', statusColors[vehicle.data_status || vehicle.status] || 'bg-gray-100 text-gray-800']"
>
{{ vehicle.data_status || vehicle.status }}
</span>
</div>
</div>
<!-- Vehicle Details -->
<div class="p-6">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-xl font-bold text-gray-900">{{ vehicle.make }} {{ vehicle.model }}</h3>
<p class="text-gray-600">{{ vehicle.year }} {{ vehicle.fuelType }}</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-blue-700">{{ vehicle.monthlyExpense }}</div>
<div class="text-sm text-gray-500">/month</div>
</div>
</div>
<!-- License Plate with Country Flag -->
<div class="mb-4">
<div class="inline-flex items-center bg-gray-100 px-4 py-2 rounded-lg space-x-3">
<img
:src="getCountryFlag(vehicle.make)"
:alt="`${vehicle.make} origin flag`"
class="w-6 h-4 rounded-sm shadow-sm"
/>
<span class="text-gray-700 font-mono font-bold text-lg">{{ vehicle.licensePlate }}</span>
</div>
</div>
<!-- Profile Completion Progress Bar (shown for all vehicles with <100% completion) -->
<div v-if="(vehicle.profile_completion_percentage || 0) < 100" class="mb-4">
<div class="flex items-center space-x-2">
<span
v-if="vehicle.data_status === 'draft'"
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="h-2 rounded-full transition-all duration-500"
:class="{
'bg-yellow-500': (vehicle.profile_completion_percentage || 0) < 50,
'bg-blue-500': (vehicle.profile_completion_percentage || 0) >= 50 && (vehicle.profile_completion_percentage || 0) < 80,
'bg-green-500': (vehicle.profile_completion_percentage || 0) >= 80
}"
:style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"
></div>
</div>
</div>
</div>
<p v-if="vehicle.data_status === 'draft'" class="text-xs text-gray-500 mt-1">Edit vehicle to provide VIN or Catalog ID</p>
<p v-else class="text-xs text-gray-500 mt-1">Complete your vehicle profile for better service recommendations</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 || 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) || 'U' }}</div>
<div class="text-sm text-gray-600">Fuel</div>
</div>
</div>
<!-- Actions -->
<div class="flex space-x-3">
<button class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors duration-200">
View Details
</button>
<button class="px-4 py-3 border border-gray-300 hover:bg-gray-50 rounded-lg transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-600" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
</div>
</div>
</div>
</template>
<style scoped>
/* Custom scrollbar for future use */
</style>

View File

@@ -1,271 +0,0 @@
<script setup>
import { computed, ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useAppModeStore } from '@/stores/appModeStore'
import { useGarageStore } from '@/stores/garageStore'
import { useAuthStore } from '@/stores/authStore'
import VehicleCard from './VehicleCard.vue'
import FleetTable from './FleetTable.vue'
import api from '@/services/api'
const router = useRouter()
const appModeStore = useAppModeStore()
const garageStore = useGarageStore()
const authStore = useAuthStore()
// Animation state
const isMounted = ref(false)
const organizations = ref([])
const activeOrganizationName = ref('')
const loadingOrganizations = ref(false)
// Fetch vehicles on component mount (simulated)
onMounted(() => {
garageStore.fetchVehicles()
// Trigger animation after mount
setTimeout(() => {
isMounted.value = true
}, 100)
// Fetch organizations if in fleet mode
if (appModeStore.isCorporateFleet) {
fetchOrganizations()
}
})
// Watch for mode changes
watch(() => appModeStore.mode, (newMode) => {
if (newMode === 'fleet') {
fetchOrganizations()
} else {
activeOrganizationName.value = ''
}
})
// Fetch user's organizations
const fetchOrganizations = async () => {
if (!authStore.token) return
try {
loadingOrganizations.value = true
const response = await api.get('/organizations/my')
organizations.value = response.data || []
// Find active organization name
const activeOrgId = authStore.activeOrgId || authStore.userProfile?.active_organization_id
if (activeOrgId && organizations.value.length > 0) {
// Try to find organization by ID
// Note: The current endpoint returns limited data, we may need to enhance it
const org = organizations.value.find(o => o.organization_id === parseInt(activeOrgId))
if (org) {
activeOrganizationName.value = org.display_name || org.name || org.full_name || `Organization #${activeOrgId}`
} else {
activeOrganizationName.value = 'Corporate Fleet'
}
} else {
activeOrganizationName.value = 'Corporate Fleet'
}
} catch (error) {
console.error('Failed to fetch organizations:', error)
activeOrganizationName.value = 'Corporate Fleet'
} finally {
loadingOrganizations.value = false
}
}
const stats = computed(() => ({
totalVehicles: garageStore.totalVehicles,
totalMonthlyExpense: garageStore.totalMonthlyExpense,
vehiclesNeedingService: garageStore.vehiclesNeedingService
}))
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(amount)
}
// Computed for header title
const headerTitle = computed(() => {
if (appModeStore.isPrivateGarage) {
return 'Saját Garázs'
} else {
if (activeOrganizationName.value && activeOrganizationName.value !== 'Corporate Fleet') {
return `Céges Flotta: ${activeOrganizationName.value}`
}
return 'Céges Flotta'
}
})
// Computed for header description
const headerDescription = computed(() => {
if (appModeStore.isPrivateGarage) {
return 'Your personal vehicle collection and maintenance tracker'
} else {
if (activeOrganizationName.value && activeOrganizationName.value !== 'Corporate Fleet') {
return `Company-wide vehicle management and cost analytics for ${activeOrganizationName.value}`
}
return 'Company-wide vehicle management and cost analytics'
}
})
</script>
<template>
<div class="space-y-8">
<!-- Header with Stats -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-6 border border-blue-100">
<div class="flex justify-between items-center mb-6">
<div>
<div class="flex items-center gap-3">
<h1 class="text-3xl font-bold text-gray-900">
{{ headerTitle }}
</h1>
<span
v-if="appModeStore.isPrivateGarage"
class="px-3 py-1 bg-gradient-to-r from-green-500 to-emerald-600 text-white text-xs font-semibold rounded-full shadow-sm"
>
B2C
</span>
<span
v-else
class="px-3 py-1 bg-gradient-to-r from-blue-500 to-indigo-600 text-white text-xs font-semibold rounded-full shadow-sm"
>
B2B
</span>
</div>
<p class="text-gray-600 mt-2">
{{ headerDescription }}
</p>
</div>
<div class="flex items-center space-x-4">
<button
@click="appModeStore.toggleMode"
class="px-4 py-2 bg-white border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-50 transition-all duration-300 flex items-center active:scale-95"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
</svg>
Switch to {{ appModeStore.isPrivateGarage ? 'Corporate' : 'Private' }} View
</button>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
v-for="(stat, index) in [
{ label: 'Total Vehicles', value: stats.totalVehicles, icon: 'check', color: 'blue' },
{ label: 'Monthly Cost', value: formatCurrency(stats.totalMonthlyExpense), icon: 'currency', color: 'green' },
{ label: 'Need Service', value: stats.vehiclesNeedingService, icon: 'warning', color: 'orange' }
]"
:key="stat.label"
class="bg-white rounded-xl p-5 shadow-sm border border-gray-200 transition-all duration-500 hover:shadow-md hover:-translate-y-1"
:style="{
opacity: isMounted ? 1 : 0,
transform: isMounted ? 'translateY(0)' : 'translateY(20px)',
transitionDelay: `${index * 100}ms`
}"
>
<div class="flex items-center">
<div :class="[`p-3 bg-${stat.color}-100 rounded-lg mr-4`]">
<svg xmlns="http://www.w3.org/2000/svg" :class="[`h-6 w-6 text-${stat.color}-600`]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path v-if="stat.icon === 'check'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<path v-if="stat.icon === 'currency'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path v-if="stat.icon === 'warning'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.998-.833-2.732 0L4.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<div class="text-2xl font-bold text-gray-900">{{ stat.value }}</div>
<div class="text-gray-600">{{ stat.label }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Dual-UI Content -->
<div v-if="appModeStore.isPrivateGarage">
<!-- Private Garage: Card Grid with TransitionGroup -->
<div>
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-900">My Vehicles</h2>
<div class="text-gray-600">
{{ garageStore.vehicles.length }} vehicles in your garage
</div>
</div>
<TransitionGroup
name="stagger-card"
tag="div"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 gap-6"
>
<VehicleCard
v-for="(vehicle, index) in garageStore.vehicles"
:key="vehicle.id"
:vehicle="vehicle"
:style="{
opacity: isMounted ? 1 : 0,
transform: isMounted ? 'translateY(0) scale(1)' : 'translateY(30px) scale(0.95)',
transitionDelay: `${index * 150}ms`
}"
class="transition-all duration-700 ease-out"
/>
</TransitionGroup>
</div>
</div>
<div v-else>
<!-- Corporate Fleet: Table View -->
<FleetTable :vehicles="garageStore.vehicles" />
</div>
<!-- Empty State (if no vehicles) -->
<div v-if="garageStore.vehicles.length === 0" class="text-center py-12">
<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
@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>
</div>
</template>
<style scoped>
/* Staggered card animations */
.stagger-card-move {
transition: transform 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.stagger-card-enter-active,
.stagger-card-leave-active {
transition: all 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.stagger-card-enter-from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
.stagger-card-leave-to {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
.stagger-card-leave-active {
position: absolute;
}
/* Smooth hover effects */
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="flex items-center gap-4 relative -mb-8 z-50 group cursor-pointer">
<svg
viewBox="0 0 240 240"
class="w-24 h-24 drop-shadow-[0_10px_20px_rgba(0,0,0,0.4)] transition-transform duration-500 group-hover:scale-105"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="sf-green" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#418890" />
<stop offset="100%" stop-color="#79B085" />
</linearGradient>
<linearGradient id="sf-white" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFFFFF" />
<stop offset="100%" stop-color="#D1D5DB" />
</linearGradient>
</defs>
<path d="M 80 160 L 30 190" stroke="url(#sf-green)" stroke-width="8" stroke-linecap="round"/>
<circle cx="15" cy="199" r="4" fill="url(#sf-green)"/>
<path d="M 105 185 L 40 225" stroke="url(#sf-green)" stroke-width="12" stroke-linecap="round"/>
<circle cx="20" cy="237" r="6" fill="url(#sf-green)"/>
<path d="M 130 205 L 80 235" stroke="url(#sf-green)" stroke-width="6" stroke-linecap="round"/>
<circle cx="65" cy="244" r="3" fill="url(#sf-green)"/>
<polygon points="120,25 105,45 135,45" fill="url(#sf-green)"/>
<polygon points="35,120 55,105 55,135" fill="url(#sf-green)"/>
<polygon points="120,215 105,195 135,195" fill="url(#sf-green)"/>
<polygon points="60,60 80,75 65,85" fill="url(#sf-green)"/>
<circle cx="120" cy="120" r="75" stroke="url(#sf-white)" stroke-width="16"/>
<text x="120" y="155" font-family="Arial, sans-serif" font-weight="900" font-size="95" text-anchor="middle">
<tspan fill="url(#sf-white)" dx="-15">S</tspan>
<tspan fill="url(#sf-green)" dx="5">F</tspan>
</text>
<path d="M 165 165 L 210 210" stroke="url(#sf-white)" stroke-width="24" stroke-linecap="round"/>
<path d="M 175 175 L 205 205" stroke="#062535" stroke-width="6" stroke-linecap="round"/>
<polygon points="215,25 130,110 100,70" fill="#2C5A63"/>
<polygon points="215,25 130,110 160,140" fill="url(#sf-green)"/>
<circle cx="130" cy="110" r="14" fill="url(#sf-white)"/>
<circle cx="130" cy="110" r="5" fill="#062535"/>
</svg>
<div class="hidden sm:flex flex-col justify-center mt-3">
<div class="font-black tracking-widest text-3xl leading-none drop-shadow-md">
<span class="text-white">SERVICE</span> <span class="text-[#75A882]">FINDER</span>
</div>
<div class="text-[#75A882] text-[0.65rem] tracking-[0.25em] font-bold mt-2 uppercase opacity-90">
Online Járműnyilvántartó & Szervizkereső
</div>
</div>
</div>
</template>
<script setup>
/**
* Service Finder - Végleges SVG Logo Komponens
* Optimalizált, kézzel írt kód <linearGradient> használatával.
*/
</script>