fix(orders): use side panel for list, bottom sheet for detail
All checks were successful
Build Docker Image / build (push) Successful in 3m56s

- orders/index.vue: Reverted to side panel pattern for map interaction
- orders/[id].vue: Converted to CatalogPage + bottom sheet pattern

List page uses #panel slot to interact with map markers.
Detail page uses fixed bottom sheet (70vh) with glass styling.
This commit is contained in:
Ruslan Bakiev
2026-01-29 18:56:31 +07:00
parent d227325d1a
commit d5aa47c323
2 changed files with 239 additions and 151 deletions

View File

@@ -1,48 +1,121 @@
<template>
<Section variant="plain">
<Stack gap="8">
<template v-if="hasOrderError">
<div class="text-sm text-error">
{{ orderError }}
<div>
<CatalogPage
:items="mapPoints"
:loading="isLoadingOrder"
:use-server-clustering="false"
map-id="order-detail-map"
point-color="#6366f1"
:show-panel="false"
:hide-view-toggle="true"
/>
<!-- Bottom Sheet -->
<div class="fixed inset-x-0 bottom-0 z-50 flex flex-col" style="height: 70vh">
<!-- Glass sheet -->
<div class="relative flex-1 bg-black/40 backdrop-blur-xl rounded-t-2xl border-t border-white/20 shadow-2xl overflow-hidden">
<!-- Drag handle -->
<div class="flex justify-center py-2">
<div class="w-12 h-1.5 bg-white/30 rounded-full" />
</div>
<Button @click="loadOrder">{{ t('ordersDetail.errors.retry') }}</Button>
</template>
<div v-else-if="isLoadingOrder" class="text-sm text-base-content/60">
{{ t('ordersDetail.states.loading') }}
</div>
<!-- Header -->
<div class="px-6 pb-4 border-b border-white/10">
<!-- Back button -->
<NuxtLink :to="localePath('/clientarea/orders')" class="inline-flex items-center gap-1 text-white/60 hover:text-white text-sm mb-3">
<Icon name="lucide:arrow-left" size="16" />
{{ t('common.back') }}
</NuxtLink>
<template v-else>
<Card padding="lg" class="border border-base-300">
<RouteSummaryHeader :title="orderTitle" :meta="orderMeta" />
</Card>
<template v-if="hasOrderError">
<div class="bg-error/20 border border-error/30 rounded-lg p-4">
<div class="font-semibold text-white mb-2">{{ t('common.error') }}</div>
<div class="text-sm text-white/70 mb-3">{{ orderError }}</div>
<button class="btn btn-sm bg-white/10 border-white/20 text-white" @click="loadOrder">
{{ t('ordersDetail.errors.retry') }}
</button>
</div>
</template>
<Card v-if="orderRoutesForMap.length" padding="lg" class="border border-base-300">
<Stack gap="4">
<RouteStagesList
:stages="orderStageItems"
:empty-text="t('ordersDetail.sections.stages.empty')"
<template v-else-if="!isLoadingOrder && order">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-xl bg-indigo-500/20 flex items-center justify-center">
<Icon name="lucide:package" size="24" class="text-indigo-400" />
</div>
<div class="flex-1 min-w-0">
<div class="font-bold text-lg text-white truncate">{{ orderTitle }}</div>
<div class="flex items-center gap-2 flex-wrap">
<span v-for="(meta, idx) in orderMeta" :key="idx" class="text-xs text-white/50">
{{ meta }}{{ idx < orderMeta.length - 1 ? ' · ' : '' }}
</span>
</div>
</div>
</div>
</template>
<template v-else>
<div class="animate-pulse">
<div class="h-12 bg-white/10 rounded-xl w-48" />
</div>
</template>
</div>
<!-- Scrollable content -->
<div v-if="!hasOrderError && order" class="overflow-y-auto h-[calc(70vh-140px)] px-6 py-4 space-y-4">
<!-- Route stages -->
<div v-if="orderStageItems.length" class="bg-white/5 rounded-xl p-4 border border-white/10">
<div class="font-semibold text-white mb-3 flex items-center gap-2">
<Icon name="lucide:route" size="18" />
{{ t('ordersDetail.sections.stages.title', 'Маршрут') }}
</div>
<div class="space-y-3">
<div
v-for="(stage, idx) in orderStageItems"
:key="stage.key || idx"
class="flex gap-3"
>
<div class="flex flex-col items-center">
<div class="w-3 h-3 rounded-full bg-indigo-500" />
<div v-if="idx < orderStageItems.length - 1" class="w-0.5 flex-1 bg-white/20 my-1" />
</div>
<div class="flex-1 pb-3">
<div class="text-sm text-white font-medium">{{ stage.from }}</div>
<div v-if="stage.to && stage.to !== stage.from" class="text-xs text-white/50 mt-0.5">
{{ stage.to }}
</div>
<div v-if="stage.meta?.length" class="text-xs text-white/40 mt-1">
{{ stage.meta.join(' · ') }}
</div>
</div>
</div>
</div>
</div>
<!-- Timeline -->
<div v-if="order.stages?.length" class="bg-white/5 rounded-xl p-4 border border-white/10">
<div class="font-semibold text-white mb-3 flex items-center gap-2">
<Icon name="lucide:calendar" size="18" />
{{ t('ordersDetail.sections.timeline.title') }}
</div>
<GanttTimeline
:stages="order.stages"
:showLoading="showLoading"
:showUnloading="showUnloading"
/>
</div>
<div class="divider my-0"></div>
<RequestRoutesMap :routes="orderRoutesForMap" :height="260" />
</Stack>
</Card>
<div class="space-y-3">
<Heading :level="3" weight="semibold">{{ t('ordersDetail.sections.timeline.title') }}</Heading>
<GanttTimeline
v-if="order?.stages"
:stages="order.stages"
:showLoading="showLoading"
:showUnloading="showUnloading"
/>
<Text v-else tone="muted">{{ t('ordersDetail.sections.timeline.empty') }}</Text>
<!-- Map preview (small) -->
<div v-if="orderRoutesForMap.length" class="bg-white/5 rounded-xl p-4 border border-white/10">
<div class="font-semibold text-white mb-3 flex items-center gap-2">
<Icon name="lucide:map" size="18" />
{{ t('ordersDetail.sections.map.title', 'Карта') }}
</div>
<RequestRoutesMap :routes="orderRoutesForMap" :height="200" />
</div>
</div>
</template>
</Stack>
</Section>
</div>
</div>
</div>
</template>
<script setup lang="ts">
@@ -52,7 +125,6 @@ import type { RouteStageItem } from '~/components/RouteStagesList.vue'
// Types from GraphQL
type OrderType = NonNullable<GetOrderQueryResult['getOrder']>
type StageType = NonNullable<NonNullable<OrderType['stages']>[number]>
type TripType = NonNullable<NonNullable<StageType['trips']>[number]>
type CompanyType = NonNullable<StageType['selectedCompany']>
definePageMeta({
@@ -62,6 +134,7 @@ definePageMeta({
const route = useRoute()
const { t } = useI18n()
const localePath = useLocalePath()
const order = ref<OrderType | null>(null)
const isLoadingOrder = ref(true)
@@ -70,6 +143,35 @@ const orderError = ref('')
const showLoading = ref(true)
const showUnloading = ref(true)
// Map points for route visualization
const mapPoints = computed(() => {
if (!order.value) return []
const points: Array<{ uuid: string; name: string; latitude: number; longitude: number }> = []
// Add source
if (order.value.sourceLatitude && order.value.sourceLongitude) {
points.push({
uuid: 'source',
name: order.value.sourceLocationName || t('ordersDetail.labels.source'),
latitude: order.value.sourceLatitude,
longitude: order.value.sourceLongitude
})
}
// Add destination
if (order.value.destinationLatitude && order.value.destinationLongitude) {
points.push({
uuid: 'destination',
name: order.value.destinationLocationName || t('ordersDetail.labels.destination'),
latitude: order.value.destinationLatitude,
longitude: order.value.destinationLongitude
})
}
return points
})
const orderTitle = computed(() => {
const source = order.value?.sourceLocationName || t('ordersDetail.labels.source_unknown')
const destination = order.value?.destinationLocationName || t('ordersDetail.labels.destination_unknown')