All checks were successful
Build Docker Image / build (push) Successful in 4m3s
- Add strictScalars: true to codegen.ts with proper scalar mappings (Date, Decimal, JSONString, JSON, UUID, BigInt → string/Record) - Replace all ref<any[]> with proper GraphQL-derived types - Add type guards for null filtering in arrays - Fix bugs exposed by typing (locationLatitude vs latitude, etc.) - Add interfaces for external components (MapboxSearchBox) This enables end-to-end type safety from GraphQL schema to frontend.
134 lines
4.4 KiB
Vue
134 lines
4.4 KiB
Vue
<template>
|
|
<Stack gap="8">
|
|
<!-- My addresses (for authenticated users) -->
|
|
<Stack v-if="isAuthenticated && teamAddresses?.length" gap="4">
|
|
<PageHeader :title="t('locations.myAddresses')">
|
|
<template #actions>
|
|
<NuxtLink
|
|
:to="localePath('/clientarea/addresses')"
|
|
class="btn btn-sm btn-ghost gap-2"
|
|
>
|
|
<Icon name="lucide:settings" size="16" />
|
|
{{ t('locations.manage') }}
|
|
</NuxtLink>
|
|
</template>
|
|
</PageHeader>
|
|
|
|
<Grid :cols="1" :md="2" :lg="3" :gap="4">
|
|
<Card
|
|
v-for="(addr, index) in teamAddresses"
|
|
:key="addr.uuid ?? index"
|
|
padding="small"
|
|
interactive
|
|
@click="selectTeamAddress(addr)"
|
|
>
|
|
<Stack gap="2">
|
|
<Stack direction="row" align="center" gap="2">
|
|
<Icon name="lucide:map-pin" size="18" class="text-primary" />
|
|
<Text size="base" weight="semibold">{{ addr.name }}</Text>
|
|
<Pill v-if="addr.isDefault" variant="outline" size="sm">{{ t('locations.default') }}</Pill>
|
|
</Stack>
|
|
<Text tone="muted" size="sm">{{ addr.address }}</Text>
|
|
</Stack>
|
|
</Card>
|
|
</Grid>
|
|
</Stack>
|
|
|
|
<!-- Terminals and logistics hubs -->
|
|
<Stack gap="4">
|
|
<PageHeader :title="t('locations.terminalsAndHubs')" />
|
|
|
|
<div v-if="pending" class="flex items-center justify-center p-8">
|
|
<span class="loading loading-spinner loading-lg" />
|
|
</div>
|
|
|
|
<Alert v-else-if="error" variant="error">
|
|
<Stack gap="2">
|
|
<Heading :level="4" weight="semibold">{{ t('locations.loadError') }}</Heading>
|
|
<Button @click="refresh()">{{ t('locations.tryAgain') }}</Button>
|
|
</Stack>
|
|
</Alert>
|
|
|
|
<EmptyState
|
|
v-else-if="!locationsData?.length"
|
|
:title="t('locations.noLocations')"
|
|
:description="t('locations.noHubsDescription')"
|
|
/>
|
|
|
|
<Grid v-else :cols="1" :md="2" :lg="3" :gap="4">
|
|
<HubCard
|
|
v-for="(location, index) in locationsData"
|
|
:key="location.uuid ?? index"
|
|
:hub="location"
|
|
selectable
|
|
@select="selectLocation(location)"
|
|
/>
|
|
</Grid>
|
|
</Stack>
|
|
</Stack>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { HubsListDocument, type HubsListQueryResult } from '~/composables/graphql/public/geo-generated'
|
|
|
|
type HubItem = NonNullable<NonNullable<HubsListQueryResult['hubsList']>[number]>
|
|
type HubWithDistance = HubItem & { distance?: string }
|
|
|
|
interface TeamAddress {
|
|
uuid?: string | null
|
|
name?: string | null
|
|
address?: string | null
|
|
isDefault?: boolean | null
|
|
}
|
|
|
|
const { t } = useI18n()
|
|
const searchStore = useSearchStore()
|
|
const { isAuthenticated } = useAuth()
|
|
const localePath = useLocalePath()
|
|
|
|
const calculateDistance = (lat: number, lng: number) => {
|
|
const moscowLat = 55.76
|
|
const moscowLng = 37.64
|
|
const distance = Math.sqrt(Math.pow(lat - moscowLat, 2) + Math.pow(lng - moscowLng, 2)) * 111
|
|
return `${Math.round(distance)} km`
|
|
}
|
|
|
|
// Load logistics hubs
|
|
const { data: locationsDataRaw, pending, error, refresh } = await useServerQuery('locations', HubsListDocument, { limit: 100 }, 'public', 'geo')
|
|
const locationsData = computed<HubWithDistance[]>(() => {
|
|
return (locationsDataRaw.value?.hubsList || [])
|
|
.filter((location): location is HubItem => location !== null)
|
|
.map((location) => ({
|
|
...location,
|
|
distance: location.latitude && location.longitude
|
|
? calculateDistance(location.latitude, location.longitude)
|
|
: undefined,
|
|
}))
|
|
})
|
|
|
|
// Load team addresses (if authenticated)
|
|
const teamAddresses = ref<TeamAddress[]>([])
|
|
|
|
if (isAuthenticated.value) {
|
|
try {
|
|
const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated')
|
|
const { data: addressData } = await useServerQuery('locations-team-addresses', GetTeamAddressesDocument, {}, 'team', 'teams')
|
|
teamAddresses.value = (addressData.value?.teamAddresses || []).filter((a): a is NonNullable<typeof a> => a !== null)
|
|
} catch (e) {
|
|
console.log('Team addresses not available')
|
|
}
|
|
}
|
|
|
|
const selectLocation = (location: HubWithDistance) => {
|
|
searchStore.setLocation(location.name)
|
|
searchStore.setLocationUuid(location.uuid)
|
|
history.back()
|
|
}
|
|
|
|
const selectTeamAddress = (addr: TeamAddress) => {
|
|
searchStore.setLocation(addr.address)
|
|
searchStore.setLocationUuid(addr.uuid)
|
|
history.back()
|
|
}
|
|
</script>
|