Files
webapp/app/components/catalog/QuoteForm.vue
Ruslan Bakiev 850ab3f252
All checks were successful
Build Docker Image / build (push) Successful in 3m52s
Add Explore/Quote dual mode to catalog page
- Add CatalogMode type (explore/quote) to useCatalogSearch
- Create ExplorePanel component with view toggle (offers/hubs/suppliers)
- Create QuoteForm and QuotePanel components for search form
- Refactor CatalogPage to fullscreen map with overlay panel
- Simplify catalog/index.vue to use new components
- Add translations for modes and quote form (ru/en)

The catalog now has two modes:
- Explore: Browse map with offers/hubs/suppliers toggle
- Quote: Search form with product/hub/qty filters to find offers
2026-01-22 19:13:45 +07:00

130 lines
3.8 KiB
Vue

<template>
<div class="flex flex-col gap-4">
<h3 class="font-semibold text-lg">{{ $t('catalog.quote.title') }}</h3>
<!-- Product chip -->
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs text-base-content/70">{{ $t('catalog.filters.product') }}</span>
</label>
<button
v-if="productLabel"
class="btn btn-outline btn-sm justify-start gap-2"
@click="emit('edit-filter', 'product')"
>
<Icon name="lucide:package" size="16" />
<span class="flex-1 text-left truncate">{{ productLabel }}</span>
<span
class="btn btn-ghost btn-xs btn-circle"
@click.stop="emit('remove-filter', 'product')"
>
<Icon name="lucide:x" size="14" />
</span>
</button>
<button
v-else
class="btn btn-ghost btn-sm justify-start gap-2 border border-dashed border-base-content/30"
@click="emit('edit-filter', 'product')"
>
<Icon name="lucide:plus" size="16" class="text-base-content/50" />
<span class="text-base-content/50">{{ $t('catalog.quote.selectProduct') }}</span>
</button>
</div>
<!-- Hub chip -->
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs text-base-content/70">{{ $t('catalog.filters.hub') }}</span>
</label>
<button
v-if="hubLabel"
class="btn btn-outline btn-sm justify-start gap-2"
@click="emit('edit-filter', 'hub')"
>
<Icon name="lucide:map-pin" size="16" />
<span class="flex-1 text-left truncate">{{ hubLabel }}</span>
<span
class="btn btn-ghost btn-xs btn-circle"
@click.stop="emit('remove-filter', 'hub')"
>
<Icon name="lucide:x" size="14" />
</span>
</button>
<button
v-else
class="btn btn-ghost btn-sm justify-start gap-2 border border-dashed border-base-content/30"
@click="emit('edit-filter', 'hub')"
>
<Icon name="lucide:plus" size="16" class="text-base-content/50" />
<span class="text-base-content/50">{{ $t('catalog.quote.selectHub') }}</span>
</button>
</div>
<!-- Quantity input -->
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs text-base-content/70">{{ $t('catalog.filters.quantity') }}</span>
</label>
<input
v-model="localQuantity"
type="number"
:placeholder="$t('catalog.quote.enterQty')"
class="input input-bordered input-sm"
min="1"
@blur="emit('update-quantity', localQuantity)"
@keyup.enter="emit('update-quantity', localQuantity)"
/>
</div>
<!-- Action buttons -->
<div class="flex gap-2 mt-2">
<button
class="btn btn-primary flex-1"
:disabled="!canSearch"
@click="emit('search')"
>
<Icon name="lucide:search" size="16" />
{{ $t('catalog.quote.search') }}
</button>
<button
v-if="hasAnyFilter"
class="btn btn-ghost btn-sm"
@click="emit('clear-all')"
>
{{ $t('catalog.quote.clear') }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
productId?: string
productLabel?: string
hubId?: string
hubLabel?: string
supplierId?: string
supplierLabel?: string
quantity?: string
canSearch: boolean
}>()
const emit = defineEmits<{
'edit-filter': [type: string]
'remove-filter': [type: string]
'update-quantity': [value: string]
'search': []
'clear-all': []
}>()
const localQuantity = ref(props.quantity || '')
watch(() => props.quantity, (newVal) => {
localQuantity.value = newVal || ''
})
const hasAnyFilter = computed(() => {
return !!(props.productId || props.hubId || props.supplierId || props.quantity)
})
</script>