refactor: remove all any types, add strict GraphQL scalar typing
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:
Ruslan Bakiev
2026-01-27 11:34:12 +07:00
parent ff34c564e1
commit 2dbe600d8a
42 changed files with 614 additions and 324 deletions

View File

@@ -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, {

View File

@@ -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 () => {

View File

@@ -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', {