Refactor: use topnav layout + CatalogPage component
All checks were successful
Build Docker Image / build (push) Successful in 4m11s
All checks were successful
Build Docker Image / build (push) Successful in 4m11s
- Remove catalog.vue layout and useCatalogLayout.ts (broken provide/inject) - All catalog/clientarea list pages now use topnav layout - Pages use CatalogPage component for SearchBar + Map functionality - Clean architecture: layout handles nav, component handles features
This commit is contained in:
@@ -1,92 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- List content -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<Card padding="lg">
|
||||
<Stack align="center" justify="center" gap="3">
|
||||
<Spinner />
|
||||
<Text tone="muted">{{ t('catalogLanding.states.loading') }}</Text>
|
||||
<CatalogPage
|
||||
:items="displayItems"
|
||||
:map-items="mapPoints"
|
||||
:loading="isLoading"
|
||||
with-map
|
||||
map-id="orders-map"
|
||||
point-color="#6366f1"
|
||||
:selected-id="selectedOrderId"
|
||||
:hovered-id="hoveredOrderId"
|
||||
:total-count="filteredItems.length"
|
||||
@select="onSelectOrder"
|
||||
@update:hovered-id="hoveredOrderId = $event"
|
||||
>
|
||||
<template #searchBar="{ displayedCount, totalCount }">
|
||||
<CatalogSearchBar
|
||||
v-model:search-query="searchQuery"
|
||||
:active-filters="activeFilterBadges"
|
||||
:displayed-count="displayedCount"
|
||||
:total-count="totalCount"
|
||||
@remove-filter="onRemoveFilter"
|
||||
@search="onSearch"
|
||||
>
|
||||
<template #filters>
|
||||
<div class="p-2 space-y-3">
|
||||
<div>
|
||||
<div class="text-xs font-semibold mb-1 text-base-content/70">{{ t('ordersList.filters.status') }}</div>
|
||||
<ul class="menu menu-compact">
|
||||
<li v-for="filter in filters" :key="filter.id">
|
||||
<a
|
||||
:class="{ 'active': selectedFilter === filter.id }"
|
||||
@click="selectedFilter = filter.id"
|
||||
>{{ filter.label }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CatalogSearchBar>
|
||||
</template>
|
||||
|
||||
<template #card="{ item }">
|
||||
<Card padding="lg" class="cursor-pointer">
|
||||
<Stack gap="4">
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.order_label') }}</Text>
|
||||
<Heading :level="3">#{{ item.name }}</Heading>
|
||||
</Stack>
|
||||
<div class="badge badge-outline">
|
||||
{{ getOrderStartDate(item) }} → {{ getOrderEndDate(item) }}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
<Grid :cols="1" :md="3" :gap="3">
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.route') }}</Text>
|
||||
<Text weight="semibold">{{ item.sourceLocationName }} → {{ item.destinationLocationName }}</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.product') }}</Text>
|
||||
<Text>
|
||||
{{ item.orderLines?.[0]?.productName || t('ordersList.card.product_loading') }}
|
||||
<template v-if="item.orderLines?.length > 1">
|
||||
<span class="badge badge-ghost ml-2">+{{ item.orderLines.length - 1 }}</span>
|
||||
</template>
|
||||
</Text>
|
||||
<Text tone="muted" size="sm">
|
||||
{{ item.orderLines?.[0]?.quantity || 0 }} {{ item.orderLines?.[0]?.unit || t('ordersList.card.unit_tons') }}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.status') }}</Text>
|
||||
<Badge :variant="getStatusVariant(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</Badge>
|
||||
<Text tone="muted" size="sm">{{ t('ordersList.card.stages_completed', { done: getCompletedStages(item), total: item.stages?.length || 0 }) }}</Text>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<Stack gap="3">
|
||||
<div
|
||||
v-for="item in displayItems"
|
||||
:key="item.uuid"
|
||||
:class="{ 'ring-2 ring-primary rounded-lg': item.uuid === selectedOrderId }"
|
||||
@click="onSelectOrder(item)"
|
||||
@mouseenter="hoveredOrderId = item.uuid"
|
||||
@mouseleave="hoveredOrderId = undefined"
|
||||
>
|
||||
<Card padding="lg" class="cursor-pointer">
|
||||
<Stack gap="4">
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.order_label') }}</Text>
|
||||
<Heading :level="3">#{{ item.name }}</Heading>
|
||||
</Stack>
|
||||
<div class="badge badge-outline">
|
||||
{{ getOrderStartDate(item) }} → {{ getOrderEndDate(item) }}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
<Grid :cols="1" :md="3" :gap="3">
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.route') }}</Text>
|
||||
<Text weight="semibold">{{ item.sourceLocationName }} → {{ item.destinationLocationName }}</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.product') }}</Text>
|
||||
<Text>
|
||||
{{ item.orderLines?.[0]?.productName || t('ordersList.card.product_loading') }}
|
||||
<template v-if="item.orderLines?.length > 1">
|
||||
<span class="badge badge-ghost ml-2">+{{ item.orderLines.length - 1 }}</span>
|
||||
</template>
|
||||
</Text>
|
||||
<Text tone="muted" size="sm">
|
||||
{{ item.orderLines?.[0]?.quantity || 0 }} {{ item.orderLines?.[0]?.unit || t('ordersList.card.unit_tons') }}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="1">
|
||||
<Text size="sm" tone="muted">{{ t('ordersList.card.status') }}</Text>
|
||||
<Badge :variant="getStatusVariant(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</Badge>
|
||||
<Text tone="muted" size="sm">{{ t('ordersList.card.stages_completed', { done: getCompletedStages(item), total: item.stages?.length || 0 }) }}</Text>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Stack v-if="displayItems.length === 0" align="center" gap="2" class="py-8">
|
||||
<EmptyState
|
||||
icon="📦"
|
||||
:title="t('orders.no_orders')"
|
||||
:description="t('orders.no_orders_desc')"
|
||||
:action-label="t('orders.create_new')"
|
||||
:action-to="localePath('/clientarea')"
|
||||
action-icon="lucide:plus"
|
||||
/>
|
||||
</Stack>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #empty>
|
||||
<EmptyState
|
||||
icon="📦"
|
||||
:title="t('orders.no_orders')"
|
||||
:description="t('orders.no_orders_desc')"
|
||||
:action-label="t('orders.create_new')"
|
||||
:action-to="localePath('/clientarea')"
|
||||
action-icon="lucide:plus"
|
||||
/>
|
||||
</template>
|
||||
</CatalogPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, defineComponent } from 'vue'
|
||||
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
|
||||
import { provideCatalogLayout } from '~/composables/useCatalogLayout'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'catalog',
|
||||
layout: 'topnav',
|
||||
middleware: ['auth-oidc']
|
||||
})
|
||||
|
||||
@@ -113,10 +129,6 @@ const searchQuery = ref('')
|
||||
const searchWithMap = ref(false)
|
||||
const currentBounds = ref<MapBounds | null>(null)
|
||||
|
||||
const onBoundsChange = (bounds: MapBounds) => {
|
||||
currentBounds.value = bounds
|
||||
}
|
||||
|
||||
// List items - one per order
|
||||
const listItems = computed(() => {
|
||||
return filteredItems.value.map(order => ({
|
||||
@@ -168,14 +180,6 @@ const displayItems = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// Hovered item with coordinates for map highlight
|
||||
const hoveredItem = computed(() => {
|
||||
if (!hoveredOrderId.value) return null
|
||||
const item = listItems.value.find(i => i.uuid === hoveredOrderId.value)
|
||||
if (!item?.latitude || !item?.longitude) return null
|
||||
return { latitude: Number(item.latitude), longitude: Number(item.longitude) }
|
||||
})
|
||||
|
||||
// Active filter badges
|
||||
const activeFilterBadges = computed(() => {
|
||||
const badges: { id: string; label: string }[] = []
|
||||
@@ -203,62 +207,6 @@ const onSelectOrder = (item: any) => {
|
||||
navigateTo(localePath(`/clientarea/orders/${item.uuid}`))
|
||||
}
|
||||
|
||||
// Handle selection from map
|
||||
const onMapSelect = (uuid: string) => {
|
||||
// Map points have -source or -dest suffix
|
||||
const orderUuid = uuid.replace(/-source$|-dest$/, '')
|
||||
const order = listItems.value.find(i => i.uuid === orderUuid)
|
||||
if (order) {
|
||||
selectedOrderId.value = orderUuid
|
||||
navigateTo(localePath(`/clientarea/orders/${orderUuid}`))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter component for the layout
|
||||
const OrdersFilterComponent = defineComponent({
|
||||
setup() {
|
||||
return () => h('div', { class: 'p-2 space-y-3' }, [
|
||||
h('div', {}, [
|
||||
h('div', { class: 'text-xs font-semibold mb-1 text-base-content/70' }, t('ordersList.filters.status')),
|
||||
h('ul', { class: 'menu menu-compact' },
|
||||
filters.value.map(filter =>
|
||||
h('li', { key: filter.id },
|
||||
h('a', {
|
||||
class: selectedFilter.value === filter.id ? 'active' : '',
|
||||
onClick: () => { selectedFilter.value = filter.id }
|
||||
}, filter.label)
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
// Provide data to layout
|
||||
provideCatalogLayout({
|
||||
// Custom subnav for clientarea
|
||||
subNavItems: [
|
||||
{ label: t('cabinetNav.orders'), path: '/clientarea/orders' },
|
||||
{ label: t('cabinetNav.addresses'), path: '/clientarea/addresses' },
|
||||
],
|
||||
searchQuery,
|
||||
activeFilters: activeFilterBadges,
|
||||
displayedCount: computed(() => displayItems.value.length),
|
||||
totalCount: computed(() => filteredItems.value.length),
|
||||
onSearch,
|
||||
onRemoveFilter,
|
||||
filterComponent: OrdersFilterComponent,
|
||||
mapItems: mapPoints,
|
||||
mapId: 'orders-map',
|
||||
pointColor: '#6366f1',
|
||||
hoveredItemId: hoveredOrderId,
|
||||
hoveredItem,
|
||||
onMapSelect,
|
||||
onBoundsChange,
|
||||
searchWithMap
|
||||
})
|
||||
|
||||
await init()
|
||||
|
||||
const getOrderStartDate = (order: any) => {
|
||||
|
||||
Reference in New Issue
Block a user