refactor: remove any types and fix TypeScript errors
All checks were successful
Build Docker Image / build (push) Successful in 3m59s

- Export InfoProductItem, InfoHubItem, InfoSupplierItem, InfoOfferItem types
- Update InfoEntity interface to have explicit fields (no index signature)
- Export CatalogHubItem, CatalogNearestHubItem from useCatalogHubs
- Fix MapItem interfaces to accept nullable GraphQL types
- Fix v-for :key bindings to handle null uuid
- Add null guards in select-location pages
- Update HubCard to accept nullable transportTypes
- Add shims.d.ts for missing module declarations
This commit is contained in:
Ruslan Bakiev
2026-01-27 10:35:14 +07:00
parent 9210f79a3d
commit 20e0e73c58
9 changed files with 133 additions and 69 deletions

View File

@@ -20,11 +20,11 @@ import { LngLatBounds } from 'mapbox-gl'
import type { ClusterPointType } from '~/composables/graphql/public/geo-generated' import type { ClusterPointType } from '~/composables/graphql/public/geo-generated'
interface MapItem { interface MapItem {
uuid: string uuid?: string | null
name: string name?: string | null
latitude: number latitude?: number | null
longitude: number longitude?: number | null
country?: string country?: string | null
} }
export interface MapBounds { export interface MapBounds {
@@ -186,11 +186,13 @@ const mapOptions = computed(() => ({
// Client-side clustering GeoJSON (when not using server clustering) // Client-side clustering GeoJSON (when not using server clustering)
const geoJsonData = computed(() => ({ const geoJsonData = computed(() => ({
type: 'FeatureCollection' as const, type: 'FeatureCollection' as const,
features: props.items.map(item => ({ features: props.items
type: 'Feature' as const, .filter(item => item.latitude != null && item.longitude != null)
properties: { uuid: item.uuid, name: item.name, country: item.country }, .map(item => ({
geometry: { type: 'Point' as const, coordinates: [item.longitude, item.latitude] } type: 'Feature' as const,
})) properties: { uuid: item.uuid, name: item.name, country: item.country },
geometry: { type: 'Point' as const, coordinates: [item.longitude!, item.latitude!] }
}))
})) }))
// Server-side clustering GeoJSON // Server-side clustering GeoJSON
@@ -481,7 +483,9 @@ const initClientClusteringLayers = async (map: MapboxMapType) => {
if (!didFitBounds.value && props.items.length > 0) { if (!didFitBounds.value && props.items.length > 0) {
const bounds = new LngLatBounds() const bounds = new LngLatBounds()
props.items.forEach(item => { props.items.forEach(item => {
bounds.extend([item.longitude, item.latitude]) if (item.longitude != null && item.latitude != null) {
bounds.extend([item.longitude, item.latitude])
}
}) })
map.fitBounds(bounds, { padding: 50, maxZoom: 10 }) map.fitBounds(bounds, { padding: 50, maxZoom: 10 })
didFitBounds.value = true didFitBounds.value = true

View File

@@ -48,7 +48,7 @@ interface Hub {
country?: string | null country?: string | null
countryCode?: string | null countryCode?: string | null
distance?: string distance?: string
transportTypes?: string[] | null transportTypes?: (string | null)[] | null
} }
const props = defineProps<{ const props = defineProps<{
@@ -81,5 +81,5 @@ const countryFlag = computed(() => {
return '🌍' return '🌍'
}) })
const hasTransport = (type: string) => props.hub.transportTypes?.includes(type) const hasTransport = (type: string) => props.hub.transportTypes?.some(t => t === type)
</script> </script>

View File

@@ -48,7 +48,7 @@
@click="emit('open-info', 'supplier', entity.teamUuid)" @click="emit('open-info', 'supplier', entity.teamUuid)"
> >
<Icon name="lucide:factory" size="14" /> <Icon name="lucide:factory" size="14" />
{{ entity.teamName || $t('catalog.info.viewSupplier') }} {{ entity.supplierName || entity.teamName || $t('catalog.info.viewSupplier') }}
</button> </button>
</div> </div>
@@ -66,8 +66,8 @@
</div> </div>
<div v-else-if="!loadingProducts" class="flex flex-col gap-2"> <div v-else-if="!loadingProducts" class="flex flex-col gap-2">
<ProductCard <ProductCard
v-for="product in relatedProducts" v-for="(product, index) in relatedProducts"
:key="product.uuid" :key="product.uuid ?? index"
:product="product" :product="product"
compact compact
selectable selectable
@@ -90,8 +90,8 @@
</div> </div>
<div v-else-if="!loadingSuppliers" class="flex flex-col gap-2"> <div v-else-if="!loadingSuppliers" class="flex flex-col gap-2">
<SupplierCard <SupplierCard
v-for="supplier in relatedSuppliers" v-for="(supplier, index) in relatedSuppliers"
:key="supplier.uuid" :key="supplier.uuid ?? index"
:supplier="supplier" :supplier="supplier"
selectable selectable
@select="onSupplierSelect(supplier)" @select="onSupplierSelect(supplier)"
@@ -113,8 +113,8 @@
</div> </div>
<div v-else-if="!loadingHubs" class="flex flex-col gap-2"> <div v-else-if="!loadingHubs" class="flex flex-col gap-2">
<HubCard <HubCard
v-for="hub in relatedHubs" v-for="(hub, index) in relatedHubs"
:key="hub.uuid" :key="hub.uuid ?? index"
:hub="hub" :hub="hub"
selectable selectable
@select="onHubSelect(hub)" @select="onHubSelect(hub)"
@@ -134,15 +134,22 @@
<script setup lang="ts"> <script setup lang="ts">
import type { InfoEntityType } from '~/composables/useCatalogSearch' import type { InfoEntityType } from '~/composables/useCatalogSearch'
import type {
InfoEntity,
InfoProductItem,
InfoHubItem,
InfoSupplierItem,
InfoOfferItem
} from '~/composables/useCatalogInfo'
const props = defineProps<{ const props = defineProps<{
entityType: InfoEntityType entityType: InfoEntityType
entityId: string entityId: string
entity: any entity: InfoEntity | null
relatedProducts?: any[] relatedProducts?: InfoProductItem[]
relatedHubs?: any[] relatedHubs?: InfoHubItem[]
relatedSuppliers?: any[] relatedSuppliers?: InfoSupplierItem[]
relatedOffers?: any[] relatedOffers?: InfoOfferItem[]
selectedProduct?: string | null selectedProduct?: string | null
currentTab?: string currentTab?: string
loading?: boolean loading?: boolean
@@ -209,20 +216,17 @@ const formatPrice = (price: number | string) => {
} }
// Handlers for selecting related items // Handlers for selecting related items
const onProductSelect = (product: any) => { const onProductSelect = (product: InfoProductItem) => {
if (product.uuid) { emit('select-product', product.uuid)
// Navigate to offer info for this product
emit('select-product', product.uuid)
}
} }
const onHubSelect = (hub: any) => { const onHubSelect = (hub: InfoHubItem) => {
if (hub.uuid) { if (hub.uuid) {
emit('open-info', 'hub', hub.uuid) emit('open-info', 'hub', hub.uuid)
} }
} }
const onSupplierSelect = (supplier: any) => { const onSupplierSelect = (supplier: InfoSupplierItem) => {
if (supplier.uuid) { if (supplier.uuid) {
emit('open-info', 'supplier', supplier.uuid) emit('open-info', 'supplier', supplier.uuid)
} }

View File

@@ -233,12 +233,11 @@ const activeClusterNodeType = computed(() => VIEW_MODE_NODE_TYPES[mapViewMode.va
const currentBounds = ref<MapBounds | null>(null) const currentBounds = ref<MapBounds | null>(null)
interface MapItem { interface MapItem {
uuid: string uuid?: string | null
latitude?: number | null latitude?: number | null
longitude?: number | null longitude?: number | null
name?: string name?: string | null
country?: string country?: string | null
[key: string]: any
} }
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{

View File

@@ -3,9 +3,13 @@ import { HubsListDocument, GetHubCountriesDocument, NearestHubsDocument } from '
const PAGE_SIZE = 24 const PAGE_SIZE = 24
// Type from codegen // Type from codegen - exported for use in pages
type HubItem = NonNullable<NonNullable<HubsListQueryResult['hubsList']>[number]> export type CatalogHubItem = NonNullable<NonNullable<HubsListQueryResult['hubsList']>[number]>
type NearestHubItem = NonNullable<NonNullable<NearestHubsQueryResult['nearestHubs']>[number]> export type CatalogNearestHubItem = NonNullable<NonNullable<NearestHubsQueryResult['nearestHubs']>[number]>
// Internal aliases
type HubItem = CatalogHubItem
type NearestHubItem = CatalogNearestHubItem
// Shared state across list and map views // Shared state across list and map views
const items = ref<Array<HubItem | NearestHubItem>>([]) const items = ref<Array<HubItem | NearestHubItem>>([])

View File

@@ -26,33 +26,43 @@ type HubItem = NonNullable<NonNullable<NearestHubsQueryResult['nearestHubs']>[nu
type OfferItem = NonNullable<NonNullable<NearestOffersQueryResult['nearestOffers']>[number]> type OfferItem = NonNullable<NonNullable<NearestOffersQueryResult['nearestOffers']>[number]>
// Product type (aggregated from offers) // Product type (aggregated from offers)
interface ProductItem { export interface InfoProductItem {
uuid: string uuid: string
name: string name: string
offersCount?: number offersCount?: number
} }
// Extended entity type with optional supplierName // Re-export types for InfoPanel
// Using intersection to allow both coordinate patterns (node vs offer) export type InfoHubItem = HubItem
interface InfoEntity { export type InfoSupplierItem = SupplierProfile
export type InfoOfferItem = OfferItem
// Extended entity type with all known fields (NO index signature!)
export interface InfoEntity {
uuid?: string | null uuid?: string | null
name?: string | null name?: string | null
// Node coordinates // Node coordinates
latitude?: number | null latitude?: number | null
longitude?: number | null longitude?: number | null
// Location fields
address?: string | null
city?: string | null
country?: string | null
// Offer coordinates (different field names) // Offer coordinates (different field names)
locationLatitude?: number | null locationLatitude?: number | null
locationLongitude?: number | null locationLongitude?: number | null
locationUuid?: string locationUuid?: string | null
locationName?: string locationName?: string | null
// Offer fields // Offer fields
productUuid?: string productUuid?: string | null
productName?: string productName?: string | null
teamUuid?: string teamUuid?: string | null
// Enriched field teamName?: string | null
supplierName?: string pricePerUnit?: number | string | null
// Allow any other fields currency?: string | null
[key: string]: unknown unit?: string | null
// Enriched field from supplier profile
supplierName?: string | null
} }
// Helper to get coordinates from entity (handles both node and offer patterns) // Helper to get coordinates from entity (handles both node and offer patterns)
@@ -73,7 +83,7 @@ export function useCatalogInfo() {
// State with proper types // State with proper types
const entity = ref<InfoEntity | null>(null) const entity = ref<InfoEntity | null>(null)
const entityType = ref<InfoEntityType | null>(null) const entityType = ref<InfoEntityType | null>(null)
const relatedProducts = ref<ProductItem[]>([]) const relatedProducts = ref<InfoProductItem[]>([])
const relatedHubs = ref<HubItem[]>([]) const relatedHubs = ref<HubItem[]>([])
const relatedSuppliers = ref<SupplierProfile[]>([]) const relatedSuppliers = ref<SupplierProfile[]>([])
const relatedOffers = ref<OfferItem[]>([]) const relatedOffers = ref<OfferItem[]>([])
@@ -119,7 +129,7 @@ export function useCatalogInfo() {
'geo' 'geo'
).then(offersData => { ).then(offersData => {
// Group offers by product // Group offers by product
const productsMap = new Map<string, ProductItem>() const productsMap = new Map<string, InfoProductItem>()
const suppliersMap = new Map<string, { uuid: string; name: string; latitude?: number | null; longitude?: number | null }>() const suppliersMap = new Map<string, { uuid: string; name: string; latitude?: number | null; longitude?: number | null }>()
offersData?.nearestOffers?.forEach(offer => { offersData?.nearestOffers?.forEach(offer => {
@@ -224,7 +234,7 @@ export function useCatalogInfo() {
'geo' 'geo'
).then(offersData => { ).then(offersData => {
// Group offers by product // Group offers by product
const productsMap = new Map<string, ProductItem>() const productsMap = new Map<string, InfoProductItem>()
offersData?.nearestOffers?.forEach(offer => { offersData?.nearestOffers?.forEach(offer => {
if (!offer || !offer.productUuid || !offer.productName) return if (!offer || !offer.productUuid || !offer.productName) return
const existing = productsMap.get(offer.productUuid) const existing = productsMap.get(offer.productUuid)

View File

@@ -78,6 +78,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useLocationStore } from '~/stores/location' import { useLocationStore } from '~/stores/location'
import type { CatalogHubItem, CatalogNearestHubItem } from '~/composables/useCatalogHubs'
import type { TeamAddress } from '~/composables/graphql/team/teams-generated'
definePageMeta({ definePageMeta({
layout: 'topnav' layout: 'topnav'
@@ -107,20 +109,20 @@ const {
} = useCatalogHubs() } = useCatalogHubs()
// Selected/hovered hub for map // Selected/hovered hub for map
const selectedHubId = ref<string>() const selectedHubId = ref<string | undefined>()
const hoveredHubId = ref<string>() const hoveredHubId = ref<string | undefined>()
await init() await init()
// Load team addresses // Load team addresses
const teamAddresses = ref<any[]>([]) const teamAddresses = ref<TeamAddress[]>([])
if (isAuthenticated.value) { if (isAuthenticated.value) {
try { try {
const { execute } = useGraphQL() const { execute } = useGraphQL()
const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated') const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated')
const data = await execute(GetTeamAddressesDocument, {}, 'team', 'teams') const data = await execute(GetTeamAddressesDocument, {}, 'team', 'teams')
teamAddresses.value = data?.teamAddresses || [] teamAddresses.value = (data?.teamAddresses || []).filter((a): a is TeamAddress => a != null)
} catch { } catch {
// Not critical // Not critical
} }
@@ -147,11 +149,12 @@ const goToRequestIfReady = () => {
return false return false
} }
const selectHub = async (hub: any) => { const selectHub = async (hub: CatalogHubItem | CatalogNearestHubItem) => {
if (!hub.uuid) return
selectedHubId.value = hub.uuid selectedHubId.value = hub.uuid
if (isSearchMode.value) { if (isSearchMode.value) {
searchStore.setLocation(hub.name) searchStore.setLocation(hub.name ?? '')
searchStore.setLocationUuid(hub.uuid) searchStore.setLocationUuid(hub.uuid)
if (goToRequestIfReady()) return if (goToRequestIfReady()) return
router.back() router.back()
@@ -159,7 +162,7 @@ const selectHub = async (hub: any) => {
} }
try { try {
const success = await locationStore.select('hub', hub.uuid, hub.name, hub.latitude, hub.longitude) const success = await locationStore.select('hub', hub.uuid, hub.name ?? '', hub.latitude ?? 0, hub.longitude ?? 0)
if (success) { if (success) {
router.back() router.back()
} }
@@ -168,7 +171,7 @@ const selectHub = async (hub: any) => {
} }
} }
const selectAddress = async (addr: any) => { const selectAddress = async (addr: TeamAddress) => {
if (isSearchMode.value) { if (isSearchMode.value) {
searchStore.setLocation(addr.address || addr.name) searchStore.setLocation(addr.address || addr.name)
searchStore.setLocationUuid(addr.uuid) searchStore.setLocationUuid(addr.uuid)
@@ -178,7 +181,7 @@ const selectAddress = async (addr: any) => {
} }
try { try {
const success = await locationStore.select('address', addr.uuid, addr.name, addr.latitude, addr.longitude) const success = await locationStore.select('address', addr.uuid, addr.name, addr.latitude ?? 0, addr.longitude ?? 0)
if (success) { if (success) {
router.back() router.back()
} }

View File

@@ -14,8 +14,8 @@
> >
<template #cards> <template #cards>
<HubCard <HubCard
v-for="hub in items" v-for="(hub, index) in items"
:key="hub.uuid" :key="hub.uuid ?? index"
:hub="hub" :hub="hub"
selectable selectable
:is-selected="selectedItemId === hub.uuid" :is-selected="selectedItemId === hub.uuid"
@@ -37,6 +37,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useLocationStore } from '~/stores/location' import { useLocationStore } from '~/stores/location'
import type { CatalogHubItem, CatalogNearestHubItem } from '~/composables/useCatalogHubs'
definePageMeta({ definePageMeta({
layout: false layout: false
@@ -64,7 +65,8 @@ await init()
const mapRef = ref<{ flyTo: (lat: number, lng: number, zoom?: number) => void } | null>(null) const mapRef = ref<{ flyTo: (lat: number, lng: number, zoom?: number) => void } | null>(null)
const selectedItemId = ref<string | null>(null) const selectedItemId = ref<string | null>(null)
const selectItem = async (item: any) => { const selectItem = async (item: CatalogHubItem | CatalogNearestHubItem) => {
if (!item.uuid) return
selectedItemId.value = item.uuid selectedItemId.value = item.uuid
if (item.latitude && item.longitude) { if (item.latitude && item.longitude) {
@@ -73,7 +75,7 @@ const selectItem = async (item: any) => {
// Selection logic // Selection logic
if (isSearchMode.value) { if (isSearchMode.value) {
searchStore.setLocation(item.name) searchStore.setLocation(item.name ?? '')
searchStore.setLocationUuid(item.uuid) searchStore.setLocationUuid(item.uuid)
if (route.query.after === 'request' && searchStore.searchForm.productUuid && searchStore.searchForm.locationUuid) { if (route.query.after === 'request' && searchStore.searchForm.productUuid && searchStore.searchForm.locationUuid) {
const query: Record<string, string> = { const query: Record<string, string> = {
@@ -92,7 +94,7 @@ const selectItem = async (item: any) => {
return return
} }
const success = await locationStore.select('hub', item.uuid, item.name, item.latitude, item.longitude) const success = await locationStore.select('hub', item.uuid, item.name ?? '', item.latitude ?? 0, item.longitude ?? 0)
if (success) router.push(localePath('/select-location')) if (success) router.push(localePath('/select-location'))
} }

38
app/shims.d.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
// Type declarations for modules without TypeScript support
declare module '@lottiefiles/dotlottie-vue' {
import type { DefineComponent } from 'vue'
export const DotLottieVue: DefineComponent<{
src?: string
autoplay?: boolean
loop?: boolean
class?: string
style?: Record<string, string | number>
}>
}
declare module 'vue-chartjs' {
import type { DefineComponent } from 'vue'
export const Line: DefineComponent
export const Bar: DefineComponent
export const Pie: DefineComponent
export const Doughnut: DefineComponent
}
declare module 'chart.js' {
export const CategoryScale: unknown
export const LinearScale: unknown
export const PointElement: unknown
export const LineElement: unknown
export const Title: unknown
export const Tooltip: unknown
export const Legend: unknown
export const Filler: unknown
export const Chart: {
register: (...args: unknown[]) => void
}
export type ChartData<T = unknown> = T
export type ChartOptions<T = unknown> = T
}