Járműkezelés majdnem kész frontend
This commit is contained in:
238
frontend/src/components/dashboard/PrivateVehicleManager.vue
Normal file
238
frontend/src/components/dashboard/PrivateVehicleManager.vue
Normal 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>
|
||||
1159
frontend/src/components/dashboard/VehicleFormModal.vue
Normal file
1159
frontend/src/components/dashboard/VehicleFormModal.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user