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

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

View File

@@ -17,6 +17,7 @@ const date = ref(new Date().toISOString().split('T')[0]) // today
const description = ref('')
const mileage = ref('')
const isLoading = ref(false)
const showDraftLimitModal = ref(false)
const handleSubmit = async () => {
if (!selectedAssetId.value) {
@@ -56,7 +57,12 @@ const handleSubmit = async () => {
emit('close')
} catch (error) {
console.error('Error saving expense:', error)
alert(`Hiba történt a mentés során: ${expenseStore.error || error.message}`)
if (error.message === 'DRAFT_LIMIT_REACHED') {
// Show custom modal for draft limit
showDraftLimitModal.value = true
} else {
alert(`Hiba történt a mentés során: ${expenseStore.error || error.message}`)
}
} finally {
isLoading.value = false
}
@@ -65,6 +71,17 @@ const handleSubmit = async () => {
const closeModal = () => {
emit('close')
}
const closeDraftLimitModal = () => {
showDraftLimitModal.value = false
}
const openEditVehicle = () => {
// TODO: Implement opening edit vehicle modal
// For now, just close the draft limit modal and maybe show a message
alert('Edit vehicle functionality to be implemented')
closeDraftLimitModal()
}
</script>
<template>
@@ -188,6 +205,39 @@ const closeModal = () => {
</div>
</div>
</div>
<!-- Draft Limit Modal -->
<div v-if="showDraftLimitModal" class="fixed inset-0 z-[200] flex items-center justify-center bg-black bg-opacity-70 p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
<div class="p-6">
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-100 text-yellow-600 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-2">You have reached the limit for unregistered vehicles! 🔒</h3>
<p class="text-gray-600 mb-6">
Please edit this vehicle and provide the VIN (Chassis Number) or exact Catalog ID to unlock full expense tracking.
</p>
</div>
<div class="flex gap-3">
<button
@click="closeDraftLimitModal"
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition"
>
Cancel
</button>
<button
@click="openEditVehicle"
class="flex-1 px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition"
>
Edit Vehicle
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>

View File

@@ -1,18 +1,231 @@
<script setup>
import { ref } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { useGarageStore } from '../../stores/garageStore'
import { useAuthStore } from '../../stores/authStore'
const emit = defineEmits(['close'])
const garageStore = useGarageStore()
const authStore = useAuthStore()
// Form fields
const make = ref('')
const model = ref('')
const generation = ref('')
const engine = ref('')
const engineId = ref(null) // NEW: Store the engine/catalog ID
const licensePlate = ref('')
const year = ref('')
const fuelType = ref('petrol')
const isLoading = ref(false)
const error = ref(null)
// Catalog data
const makes = ref([])
const models = ref([])
const generations = ref([])
const engines = ref([])
// Loading states
const loadingMakes = ref(false)
const loadingModels = ref(false)
const loadingGenerations = ref(false)
const loadingEngines = ref(false)
// Search filters
const makeSearch = ref('')
const modelSearch = ref('')
const generationSearch = ref('')
const engineSearch = ref('')
// Filtered lists based on search
const filteredMakes = ref([])
const filteredModels = ref([])
const filteredGenerations = ref([])
const filteredEngines = ref([])
// Fetch makes on component mount
onMounted(async () => {
await fetchMakes()
})
// Watch for search changes
watch(makeSearch, (val) => {
filteredMakes.value = makes.value.filter(m =>
m.toLowerCase().includes(val.toLowerCase())
)
})
watch(modelSearch, (val) => {
filteredModels.value = models.value.filter(m =>
m.toLowerCase().includes(val.toLowerCase())
)
})
watch(generationSearch, (val) => {
filteredGenerations.value = generations.value.filter(g =>
g.toLowerCase().includes(val.toLowerCase())
)
})
watch(engineSearch, (val) => {
filteredEngines.value = engines.value.filter(e =>
e.variant.toLowerCase().includes(val.toLowerCase())
)
})
// Watch for make selection to fetch models
watch(make, async (newMake) => {
if (newMake) {
await fetchModels(newMake)
model.value = ''
generation.value = ''
engine.value = ''
engineId.value = null
generations.value = []
engines.value = []
} else {
models.value = []
generations.value = []
engines.value = []
}
})
// Watch for model selection to fetch generations
watch(model, async (newModel) => {
if (newModel && make.value) {
await fetchGenerations(make.value, newModel)
generation.value = ''
engine.value = ''
engineId.value = null
engines.value = []
} else {
generations.value = []
engines.value = []
}
})
// Watch for generation selection to fetch engines
watch(generation, async (newGen) => {
if (newGen && make.value && model.value) {
await fetchEngines(make.value, model.value, newGen)
engine.value = ''
engineId.value = null
} else {
engines.value = []
}
})
// Watch for engine selection to auto-fill fuel type and capture ID
watch(engine, (newEngine) => {
if (newEngine) {
const selectedEngine = engines.value.find(e => e.variant === newEngine)
if (selectedEngine) {
// Store the engine ID for API call
engineId.value = selectedEngine.id
// Auto-fill fuel type if available
if (selectedEngine.fuel_type) {
fuelType.value = selectedEngine.fuel_type.toLowerCase()
}
}
} else {
engineId.value = null
}
})
// API calls
async function fetchMakes() {
loadingMakes.value = true
try {
const token = authStore.token
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
const response = await fetch(`${apiBaseUrl}/catalog/makes`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
})
if (!response.ok) throw new Error(`Failed to fetch makes: ${response.status}`)
makes.value = await response.json()
filteredMakes.value = makes.value
} catch (err) {
console.error('Error fetching makes:', err)
error.value = 'Could not load makes. Please try again.'
} finally {
loadingMakes.value = false
}
}
async function fetchModels(makeName) {
loadingModels.value = true
try {
const token = authStore.token
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
const response = await fetch(`${apiBaseUrl}/catalog/models?make=${encodeURIComponent(makeName)}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
})
if (!response.ok) throw new Error(`Failed to fetch models: ${response.status}`)
models.value = await response.json()
filteredModels.value = models.value
} catch (err) {
console.error('Error fetching models:', err)
error.value = 'Could not load models for this make.'
} finally {
loadingModels.value = false
}
}
async function fetchGenerations(makeName, modelName) {
loadingGenerations.value = true
try {
const token = authStore.token
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
const response = await fetch(`${apiBaseUrl}/catalog/generations?make=${encodeURIComponent(makeName)}&model=${encodeURIComponent(modelName)}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
})
if (!response.ok) throw new Error(`Failed to fetch generations: ${response.status}`)
generations.value = await response.json()
filteredGenerations.value = generations.value
} catch (err) {
console.error('Error fetching generations:', err)
error.value = 'Could not load generations for this model.'
} finally {
loadingGenerations.value = false
}
}
async function fetchEngines(makeName, modelName, genName) {
loadingEngines.value = true
try {
const token = authStore.token
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
const response = await fetch(`${apiBaseUrl}/catalog/engines?make=${encodeURIComponent(makeName)}&model=${encodeURIComponent(modelName)}&gen=${encodeURIComponent(genName)}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
})
if (!response.ok) throw new Error(`Failed to fetch engines: ${response.status}`)
const engineData = await response.json()
engines.value = engineData.map(e => ({
variant: e.variant,
fuel_type: e.fuel_type,
id: e.id
}))
filteredEngines.value = engines.value
} catch (err) {
console.error('Error fetching engines:', err)
error.value = 'Could not load engines for this generation.'
} finally {
loadingEngines.value = false
}
}
const handleSubmit = async () => {
error.value = null
isLoading.value = true
@@ -23,8 +236,14 @@ const handleSubmit = async () => {
make: make.value,
model: model.value,
licensePlate: licensePlate.value,
year: parseInt(year.value),
fuelType: fuelType.value
year: parseInt(year.value) || new Date().getFullYear(),
fuelType: fuelType.value,
generation: generation.value,
engine: engine.value,
// Include the catalog ID (engine ID) for API
catalogId: engineId.value,
// Include organization ID (use active organization ID)
organizationId: authStore.activeOrgId
}
// Call the garage store to add vehicle
@@ -36,9 +255,16 @@ const handleSubmit = async () => {
// Reset form
make.value = ''
model.value = ''
generation.value = ''
engine.value = ''
engineId.value = null
licensePlate.value = ''
year.value = ''
fuelType.value = 'petrol'
makes.value = []
models.value = []
generations.value = []
engines.value = []
// Close modal
emit('close')

View File

@@ -14,18 +14,19 @@ const themeClasses = themeStore.themeClasses
const statusColors = {
'OK': 'bg-green-100 text-green-800',
'Service Due': 'bg-blue-100 text-blue-900',
'Warning': 'bg-orange-100 text-orange-800'
'Warning': 'bg-orange-100 text-orange-800',
'draft': 'bg-yellow-100 text-yellow-800'
}
const brandLogoUrl = (make) => {
const cleanMake = make.toLowerCase().replace(/\s+/g, '')
const cleanMake = (make || '').toLowerCase().replace(/\s+/g, '')
// Use simpleicons CDN
return `https://cdn.simpleicons.org/${cleanMake}`
}
// Country flag mapping
const getCountryFlag = (make) => {
const makeLower = make.toLowerCase()
const makeLower = (make || '').toLowerCase()
if (makeLower.includes('bmw') || makeLower.includes('mercedes') || makeLower.includes('audi') || makeLower.includes('volkswagen') || makeLower.includes('porsche')) {
return 'https://flagcdn.com/w40/de.png'
} else if (makeLower.includes('tesla') || makeLower.includes('ford') || makeLower.includes('chevrolet') || makeLower.includes('dodge')) {
@@ -98,14 +99,28 @@ const getCountryFlag = (make) => {
</div>
</div>
<!-- Draft Status & Profile Completion -->
<div v-if="vehicle.status === 'draft'" class="mb-4">
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">DRAFT</span>
<div class="flex-1">
<div class="text-xs text-gray-600 mb-1">Profile: {{ vehicle.profile_completion_percentage || 0 }}% Complete</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-yellow-500 h-2 rounded-full" :style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"></div>
</div>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Edit vehicle to provide VIN or Catalog ID</p>
</div>
<!-- Stats -->
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-gray-900">{{ (vehicle.mileage / 1000).toFixed(1) }}k</div>
<div class="text-2xl font-bold text-gray-900">{{ ((vehicle.mileage || 0) / 1000).toFixed(1) }}k</div>
<div class="text-sm text-gray-600">km</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-gray-900">{{ vehicle.fuelType.charAt(0) }}</div>
<div class="text-2xl font-bold text-gray-900">{{ vehicle.fuelType?.charAt(0) || 'U' }}</div>
<div class="text-sm text-gray-600">Fuel</div>
</div>
</div>

View File

@@ -1,10 +1,13 @@
<script setup>
import { computed, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAppModeStore } from '@/stores/appModeStore'
import { useGarageStore } from '@/stores/garageStore'
import VehicleCard from './VehicleCard.vue'
import FleetTable from './FleetTable.vue'
const router = useRouter()
const appModeStore = useAppModeStore()
const garageStore = useGarageStore()
@@ -138,7 +141,10 @@ const formatCurrency = (amount) => {
<div class="text-6xl text-gray-300 mb-4">🚗</div>
<h3 class="text-xl font-bold text-gray-500 mb-2">No vehicles yet</h3>
<p class="text-gray-600 mb-6">Add your first vehicle to get started</p>
<button class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-all duration-300 hover:scale-105 active:scale-95">
<button
@click="router.push('/vehicles/add')"
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-all duration-300 hover:scale-105 active:scale-95"
>
Add First Vehicle
</button>
</div>