refactor: remove all any types, add strict GraphQL scalar typing
All checks were successful
Build Docker Image / build (push) Successful in 4m3s
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.
This commit is contained in:
@@ -44,7 +44,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetNodeDocument, NearestOffersDocument } from '~/composables/graphql/public/geo-generated'
|
||||
import { GetNodeDocument, NearestOffersDocument, type OfferWithRouteType, type GetNodeQueryResult } from '~/composables/graphql/public/geo-generated'
|
||||
|
||||
type Hub = NonNullable<GetNodeQueryResult['node']>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav'
|
||||
@@ -56,7 +58,7 @@ const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(true)
|
||||
const hoveredId = ref<string>()
|
||||
const hub = ref<any>(null)
|
||||
const hub = ref<Hub | null>(null)
|
||||
const products = ref<Array<{ uuid: string; name: string }>>([])
|
||||
|
||||
const hubId = computed(() => route.params.id as string)
|
||||
@@ -150,7 +152,7 @@ try {
|
||||
|
||||
// Group offers by product
|
||||
const productsMap = new Map<string, { uuid: string; name: string }>()
|
||||
offersData.value?.nearestOffers?.forEach((offer: any) => {
|
||||
offersData.value?.nearestOffers?.forEach((offer) => {
|
||||
if (offer?.productUuid) {
|
||||
if (!productsMap.has(offer.productUuid)) {
|
||||
productsMap.set(offer.productUuid, {
|
||||
|
||||
@@ -37,19 +37,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Location on map -->
|
||||
<div v-if="offer.latitude && offer.longitude" class="h-48 rounded-lg overflow-hidden">
|
||||
<div v-if="offer.locationLatitude && offer.locationLongitude" class="h-48 rounded-lg overflow-hidden">
|
||||
<ClientOnly>
|
||||
<MapboxMap
|
||||
map-id="offer-location-map"
|
||||
class="w-full h-full"
|
||||
:options="{
|
||||
style: 'mapbox://styles/mapbox/streets-v12',
|
||||
center: [offer.longitude, offer.latitude],
|
||||
center: [offer.locationLongitude, offer.locationLatitude],
|
||||
zoom: 8
|
||||
}"
|
||||
>
|
||||
<MapboxDefaultMarker
|
||||
:lnglat="[offer.longitude, offer.latitude]"
|
||||
:lnglat="[offer.locationLongitude, offer.locationLatitude]"
|
||||
color="#10b981"
|
||||
/>
|
||||
</MapboxMap>
|
||||
@@ -101,7 +101,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetOfferDocument, GetSupplierProfileByTeamDocument } from '~/composables/graphql/public/exchange-generated'
|
||||
import { GetOfferDocument, GetSupplierProfileByTeamDocument, type GetOfferQueryResult, type GetSupplierProfileByTeamQueryResult } from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
type Offer = NonNullable<GetOfferQueryResult['getOffer']>
|
||||
type SupplierProfile = NonNullable<GetSupplierProfileByTeamQueryResult['getSupplierProfileByTeam']>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav'
|
||||
@@ -114,8 +117,8 @@ const { execute } = useGraphQL()
|
||||
const offerId = computed(() => route.params.offerId as string)
|
||||
|
||||
const isLoading = ref(true)
|
||||
const offer = ref<any>(null)
|
||||
const supplier = ref<any>(null)
|
||||
const offer = ref<Offer | null>(null)
|
||||
const supplier = ref<SupplierProfile | null>(null)
|
||||
|
||||
// Load offer data
|
||||
const loadOffer = async () => {
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</Stack>
|
||||
|
||||
<!-- Line with this product -->
|
||||
<template v-for="line in getProductLines(offer)" :key="line?.uuid">
|
||||
<template v-for="(line, lineIndex) in getProductLines(offer)" :key="line?.uuid ?? lineIndex">
|
||||
<Card padding="sm" class="bg-base-200">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack gap="0">
|
||||
@@ -204,8 +204,14 @@ import {
|
||||
GetProductsDocument,
|
||||
GetProductOffersDocument,
|
||||
GetSupplierProfilesDocument,
|
||||
type GetProductsQueryResult,
|
||||
type GetProductOffersQueryResult
|
||||
} from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
// Types from GraphQL
|
||||
type Product = NonNullable<NonNullable<GetProductsQueryResult['getProducts']>[number]>
|
||||
type ProductOffer = NonNullable<NonNullable<GetProductOffersQueryResult['getOffers']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav'
|
||||
})
|
||||
@@ -237,7 +243,7 @@ const allSuppliers = computed(() => suppliersData.value?.getSupplierProfiles ||
|
||||
const productId = computed(() => route.params.id as string)
|
||||
|
||||
// Find product by uuid from list
|
||||
const findProduct = (products: any[]) => {
|
||||
const findProduct = (products: (Product | null)[]) => {
|
||||
return products.find(p => p?.uuid === productId.value)
|
||||
}
|
||||
|
||||
@@ -295,11 +301,13 @@ const mapLocations = computed(() => {
|
||||
const priceRange = computed(() => {
|
||||
const prices: number[] = []
|
||||
offers.value.forEach(offer => {
|
||||
(offer as any).lines?.forEach((line: any) => {
|
||||
if (line?.productUuid === productId.value && line?.pricePerUnit) {
|
||||
prices.push(Number(line.pricePerUnit))
|
||||
// Offers for this product already filtered by productUuid
|
||||
if (offer.pricePerUnit) {
|
||||
const price = typeof offer.pricePerUnit === 'string' ? parseFloat(offer.pricePerUnit) : Number(offer.pricePerUnit)
|
||||
if (!isNaN(price)) {
|
||||
prices.push(price)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if (prices.length === 0) return t('common.values.not_available')
|
||||
const min = Math.min(...prices)
|
||||
@@ -308,9 +316,24 @@ const priceRange = computed(() => {
|
||||
return t('catalogProduct.labels.price_range', { min: min.toLocaleString(), max: max.toLocaleString() })
|
||||
})
|
||||
|
||||
// Get lines with this product
|
||||
const getProductLines = (offer: any) => {
|
||||
return (offer.lines || []).filter((line: any) => line?.productUuid === productId.value)
|
||||
// Get offer as "line" - offers already have quantity/unit/price directly
|
||||
interface OfferLine {
|
||||
uuid?: string | null
|
||||
quantity?: string | number | null
|
||||
unit?: string | null
|
||||
pricePerUnit?: string | number | null
|
||||
currency?: string | null
|
||||
}
|
||||
|
||||
const getProductLines = (offer: ProductOffer): OfferLine[] => {
|
||||
// Each offer is a single "line" with quantity, unit, and price
|
||||
return [{
|
||||
uuid: offer.uuid,
|
||||
quantity: offer.quantity,
|
||||
unit: offer.unit,
|
||||
pricePerUnit: offer.pricePerUnit,
|
||||
currency: offer.currency
|
||||
}]
|
||||
}
|
||||
|
||||
const getCategoryIcon = (categoryName: string | null | undefined) => {
|
||||
@@ -355,9 +378,10 @@ const formatDate = (dateStr: string | null | undefined) => {
|
||||
}
|
||||
}
|
||||
|
||||
const formatPrice = (price: any, currency: string | null | undefined) => {
|
||||
const formatPrice = (price: string | number | null | undefined, currency: string | null | undefined) => {
|
||||
if (!price) return '—'
|
||||
const num = Number(price)
|
||||
const num = typeof price === 'string' ? parseFloat(price) : Number(price)
|
||||
if (isNaN(num)) return '—'
|
||||
const curr = currency || 'USD'
|
||||
try {
|
||||
return new Intl.NumberFormat('ru', {
|
||||
|
||||
Reference in New Issue
Block a user