2026.06.04 frontend építés közben
This commit is contained in:
@@ -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">
|
||||
×
|
||||
</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>
|
||||
624
frontend/src/components/LoginModal.vue
Normal file
624
frontend/src/components/LoginModal.vue
Normal 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>
|
||||
66
frontend/src/components/Logo.vue
Normal file
66
frontend/src/components/Logo.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
69
frontend/src/components/logo1.vue
Normal file
69
frontend/src/components/logo1.vue
Normal 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>
|
||||
Reference in New Issue
Block a user