Járműkezelés majdnem kész frontend

This commit is contained in:
Roo
2026-06-11 11:22:47 +00:00
parent 90e3173fbc
commit 0a3fd8de74
18 changed files with 2749 additions and 99 deletions

View File

@@ -0,0 +1,238 @@
<template>
<div class="flex flex-col h-full">
<!-- Header: Title + Counter + Add Button (nav arrows removed) -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<h2 class="text-2xl font-bold text-slate-800">Saját Járművek</h2>
<span
v-if="vehicles.length > 0"
class="inline-flex items-center rounded-full bg-sf-accent/10 px-3 py-1 text-xs font-semibold text-sf-accent"
>
{{ vehicles.length }} jármű
</span>
</div>
<div class="flex items-center gap-3">
<button
@click="isAddFormOpen = true"
class="inline-flex items-center gap-2 rounded-xl bg-sf-accent px-5 py-2.5 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Új jármű hozzáadása
</button>
</div>
</div>
<!-- Carousel (with vehicles) or Empty State -->
<template v-if="vehicles.length > 0">
<div class="relative">
<!-- Left Edge Navigation Strip (only show when scrolling is needed) -->
<div
v-if="vehicles.length > 2"
@click="scrollLeft"
class="absolute left-0 top-0 bottom-0 w-12 bg-gradient-to-r from-white/80 to-transparent flex items-center justify-start cursor-pointer z-10 rounded-l-2xl"
>
<svg class="h-6 w-6 text-slate-500 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</div>
<!-- Right Edge Navigation Strip (only show when scrolling is needed) -->
<div
v-if="vehicles.length > 2"
@click="scrollRight"
class="absolute right-0 top-0 bottom-0 w-12 bg-gradient-to-l from-white/80 to-transparent flex items-center justify-end cursor-pointer z-10 rounded-r-2xl"
>
<svg class="h-6 w-6 text-slate-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
<div
ref="carouselRef"
class="flex overflow-x-auto snap-x snap-mandatory gap-6 pb-4 w-full scroll-smooth no-scrollbar"
:class="vehicles.length > 2 ? 'px-12' : 'px-2'"
style="scrollbar-width: none; -ms-overflow-style: none;"
>
<VehicleCardStandard
v-for="vehicle in vehicles"
:key="vehicle.id"
:vehicle="vehicle"
@click="openDetailView(vehicle)"
@edit="openEditDirect(vehicle)"
@set-primary="setPrimaryVehicle"
/>
</div>
</div>
</template>
<!-- Empty State (v-else) -->
<template v-else>
<div class="flex flex-1 flex-col items-center justify-center text-slate-400">
<svg class="mb-4 h-20 w-20 text-slate-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h3 class="text-lg font-semibold text-slate-500">Még nincsenek járművek</h3>
<p class="mt-1 text-sm text-slate-400">Kattints az "Új jármű hozzáadása" gombra az első jármű felvételéhez.</p>
<button
@click="isAddFormOpen = true"
class="mt-6 inline-flex items-center gap-2 rounded-xl bg-sf-accent px-6 py-3 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Új jármű hozzáadása
</button>
</div>
</template>
<!-- Vehicle Form Modal (Add) -->
<VehicleFormModal
:is-open="isAddFormOpen"
@close="isAddFormOpen = false"
@saved="onVehicleSaved"
/>
<!-- Vehicle Form Modal (Edit) -->
<VehicleFormModal
:is-open="isEditFormOpen"
:vehicle="editingVehicle"
@close="isEditFormOpen = false"
@saved="onVehicleEdited"
/>
<!-- Vehicle Detail Modal (read-only) -->
<VehicleDetailModal
:is-open="isDetailOpen"
:vehicle="detailVehicle"
@close="isDetailOpen = false"
@edit="openEditFromDetail"
@set-primary="setPrimaryVehicle"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, onMounted, watch } from 'vue'
import type { VehicleData } from '../../types/vehicle'
import { useVehicleStore } from '../../stores/vehicle'
import { mockVehicles } from '../../data/mockVehicles'
import VehicleFormModal from './VehicleFormModal.vue'
import VehicleDetailModal from '../vehicle/VehicleDetailModal.vue'
import VehicleCardStandard from '../vehicle/VehicleCardStandard.vue'
const props = defineProps<{
targetVehicleId?: string | null
}>()
const vehicleStore = useVehicleStore()
/**
* Smart vehicle list: real backend vehicles first, then mock data as fallback.
* Deduplicates by license_plate to avoid showing the same vehicle twice.
*/
const vehicles = computed<VehicleData[]>(() => {
const real = vehicleStore.vehicles as unknown as VehicleData[]
if (real.length >= 3) return real
const needed = 3 - real.length
const mocksToUse = mockVehicles
.filter(mv => !real.some(rv => (rv.license_plate || rv.licensePlate) === (mv.license_plate || mv.licensePlate)))
.slice(0, needed)
return [...real, ...mocksToUse]
})
// ── Carousel ref & scroll functions ──
const carouselRef = ref<HTMLElement | null>(null)
function scrollLeft() {
if (carouselRef.value) {
carouselRef.value.scrollBy({ left: -300, behavior: 'smooth' })
}
}
function scrollRight() {
if (carouselRef.value) {
carouselRef.value.scrollBy({ left: 300, behavior: 'smooth' })
}
}
// ── Detail Modal state ──
const isDetailOpen = ref(false)
const detailVehicle = ref<VehicleData | null>(null)
function openDetailView(vehicle: VehicleData) {
detailVehicle.value = vehicle
isDetailOpen.value = true
}
// ── Direct Edit (from card edit button) ──
function openEditDirect(vehicle: VehicleData) {
editingVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
}
// ── Bridge: Detail → Edit ──
async function openEditFromDetail(vehicle: VehicleData) {
isDetailOpen.value = false
// Use nextTick so detail modal closes before edit opens
await nextTick()
editingVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
}
// ── Modal state (Add) ──
const isAddFormOpen = ref(false)
function onVehicleSaved() {
isAddFormOpen.value = false
// In real implementation, refresh the vehicle list from the store
}
// ── Modal state (Edit) ──
const isEditFormOpen = ref(false)
const editingVehicle = ref<VehicleData | null>(null)
function onVehicleEdited() {
isEditFormOpen.value = false
editingVehicle.value = null
// In real implementation, refresh the vehicle list from the store
}
// ── Targeted scroll to vehicle card ──
function scrollToVehicle(licensePlate: string) {
const el = document.getElementById('vehicle-card-' + licensePlate)
if (el) {
el.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'center' })
}
}
onMounted(() => {
if (props.targetVehicleId) {
// Small delay to ensure DOM is rendered
setTimeout(() => scrollToVehicle(props.targetVehicleId!), 100)
}
})
watch(() => props.targetVehicleId, (newVal) => {
if (newVal) {
setTimeout(() => scrollToVehicle(newVal), 100)
}
})
// ── Set Primary Vehicle ──
async function setPrimaryVehicle(vehicle: VehicleData) {
console.log('[PrivateVehicleManager] setPrimaryVehicle called for:', vehicle.id, vehicle.license_plate)
// TODO: Real API call — PUT /api/v1/assets/vehicles/{id} with { is_primary: true }
// This will also need to unset any other primary vehicle for this user.
// For now, just log and update local state optimistically.
vehicleStore.setPrimaryVehicle(vehicle.id)
}
</script>
<style scoped>
/* Hide scrollbar for the carousel container */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
</style>

File diff suppressed because it is too large Load Diff