Files
hms-licht-ton/frontend/pages/mietkatalog.vue
T

105 lines
5.1 KiB
Vue
Raw Normal View History

<template>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8">
<div><h1 class="text-3xl sm:text-4xl font-bold mb-2" style="color: var(--text)">Mietkatalog</h1><p style="color: var(--secondary)">Durchsuchen Sie unser Equipment-Sortiment und stellen Sie Ihre Mietanfrage zusammen.</p></div>
<button @click="navigate('/warenkorb')" class="hms-btn hms-btn-secondary text-sm self-start sm:self-auto">🛒 Warenkorb ansehen</button>
</div>
<div class="hms-card p-4 mb-6">
<div class="flex flex-col lg:flex-row gap-4">
<div class="relative flex-1"><svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5" style="color: var(--secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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><input v-model="searchQuery" type="search" class="hms-input pl-10" placeholder="Gerät, Marke oder Artikelnummer suchen..." aria-label="Equipment suchen" @input="onSearchInput" /></div>
<select v-model="sortBy" class="hms-input lg:w-48" aria-label="Sortieren nach"><option value="name_asc">Sortieren: Name (A-Z)</option><option value="name_desc">Sortieren: Name (Z-A)</option></select>
</div>
<div v-if="categories.length > 1" class="flex flex-wrap gap-2 mt-4" role="tablist" aria-label="Kategorie-Filter"><button v-for="cat in categories" :key="cat" @click="activeCategory = cat; reload()" :class="['hms-chip', activeCategory === cat ? 'active' : '']" role="tab" :aria-selected="activeCategory === cat">{{ cat === 'alle' ? 'Alle Kategorien' : cat }}</button></div>
</div>
<div v-if="!pending && !error" class="text-sm mb-4" style="color: var(--secondary)" aria-live="polite">{{ total }} {{ total === 1 ? 'Gerät' : 'Geräte' }} gefunden</div>
<LoadingSkeleton v-if="pending" :count="6" />
<ErrorState v-else-if="error" title="Katalog nicht erreichbar" message="Der Equipment-Katalog ist aktuell nicht verfügbar." @retry="reload" />
<EmptyState v-else-if="equipment.length === 0" icon="🔍" title="Keine Geräte gefunden" message="Für Ihre Suchanfrage wurden keine Geräte gefunden." action-label="Filter zurücksetzen" @action="searchQuery=''; activeCategory='alle'; reload()" />
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"><EquipmentCard v-for="item in equipment" :key="item.id" :item="mapItem(item)" @click="navigateToDetail" @add-to-cart="addToCart" /></div>
<div v-if="totalPages > 1" class="flex items-center justify-center gap-2 mt-8">
<button :disabled="currentPage <= 1" @click="currentPage--; reload()" class="hms-btn hms-btn-secondary px-4 py-2 disabled:opacity-40" aria-label="Vorherige Seite"></button>
<span class="text-sm" style="color: var(--secondary)">Seite {{ currentPage }} von {{ totalPages }}</span>
<button :disabled="currentPage >= totalPages" @click="currentPage++; reload()" class="hms-btn hms-btn-secondary px-4 py-2 disabled:opacity-40" aria-label="Nächste Seite"></button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useEquipment } from '~/composables/useEquipment'
import { useCart } from '~/composables/useCart'
const { list, categories: fetchCategories } = useEquipment()
const { addItemByFields } = useCart()
const searchQuery = ref('')
const activeCategory = ref('alle')
const sortBy = ref('name_asc')
const currentPage = ref(1)
let searchTimeout: ReturnType<typeof setTimeout> | null = null
const categories = ref<string[]>(['alle'])
const { data, pending, error, refresh } = await useAsyncData(
'equipment-list',
() => list({
search: searchQuery.value.trim() || undefined,
category: activeCategory.value !== 'alle' ? activeCategory.value : undefined,
sort: sortBy.value as any,
page: currentPage.value,
page_size: 24
}),
{ default: () => ({ items: [], total: 0, page: 1, page_size: 24, total_pages: 0 }) }
)
const equipment = computed(() => data.value?.items || [])
const total = computed(() => data.value?.total || 0)
const totalPages = computed(() => data.value?.total_pages || 0)
function reload() {
refresh()
}
function onSearchInput() {
if (searchTimeout) clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
currentPage.value = 1
reload()
}, 300)
}
useAsyncData('equipment-categories', async () => {
try {
const cats = await fetchCategories()
if (cats && cats.length > 0) {
categories.value = ['alle', ...cats.filter((c: string) => c && c.trim() !== '')]
}
} catch {}
})
function mapItem(item: any) {
return {
...item,
code: item.number || item.rentman_id,
image: item.image_url || ''
}
}
function navigate(route: string) {
navigateTo(route)
if (import.meta.client) window.scrollTo(0, 0)
}
function navigateToDetail(item: any) {
navigateTo('/mietkatalog/' + item.id)
if (import.meta.client) window.scrollTo(0, 0)
}
function addToCart(item: any) {
addItemByFields({
equipment_id: item.id,
name: item.name,
rental_price: null,
image_url: item.image_url || null
})
}
</script>