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

@@ -47,13 +47,22 @@ interface BankData {
correspondentAccount: string
}
interface BankSuggestion {
value: string
data: {
bic: string
correspondent_account?: string
address?: { value: string }
}
}
interface Props {
modelValue?: BankData
}
interface Emits {
(e: 'update:modelValue', value: BankData): void
(e: 'select', bank: any): void
(e: 'select', bank: BankSuggestion): void
}
const props = withDefaults(defineProps<Props>(), {
@@ -66,15 +75,6 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>()
interface BankSuggestion {
value: string
data: {
bic: string
correspondent_account?: string
address?: { value: string }
}
}
const query = ref('')
const suggestions = ref<BankSuggestion[]>([])
const loading = ref(false)
@@ -123,7 +123,7 @@ const onInput = async () => {
}
}
const selectBank = (bank: any) => {
const selectBank = (bank: BankSuggestion) => {
query.value = bank.value
showDropdown.value = false

View File

@@ -22,7 +22,7 @@
:key="option.sourceUuid ?? index"
:location-name="getOfferData(option.sourceUuid)?.locationName"
:product-name="productName"
:price-per-unit="getOfferData(option.sourceUuid)?.pricePerUnit"
:price-per-unit="parseFloat(getOfferData(option.sourceUuid)?.pricePerUnit || '0') || null"
:currency="getOfferData(option.sourceUuid)?.currency"
:unit="getOfferData(option.sourceUuid)?.unit"
:stages="getRouteStages(option)"
@@ -81,7 +81,8 @@ interface RoutePathType {
totalTimeSeconds?: number | null
stages?: (RouteStage | null)[]
}
import { GetOfferDocument, GetSupplierProfileByTeamDocument } from '~/composables/graphql/public/exchange-generated'
import { GetOfferDocument, GetSupplierProfileByTeamDocument, type GetOfferQueryResult, type GetSupplierProfileByTeamQueryResult } from '~/composables/graphql/public/exchange-generated'
import type { OfferWithRouteType, RouteStageType } from '~/composables/graphql/public/geo-generated'
const route = useRoute()
const localePath = useLocalePath()
@@ -90,12 +91,14 @@ const { execute } = useGraphQL()
const productName = computed(() => searchStore.searchForm.product || (route.query.product as string) || 'Товар')
const locationName = computed(() => searchStore.searchForm.location || (route.query.location as string) || 'Назначение')
const quantity = computed(() => (route.query.quantity as string) || (searchStore.searchForm as any)?.quantity)
const quantity = computed(() => (route.query.quantity as string) || searchStore.searchForm.quantity)
// Offer data for prices
const offersData = ref<Map<string, any>>(new Map())
type OfferData = NonNullable<GetOfferQueryResult['getOffer']>
const offersData = ref<Map<string, OfferData>>(new Map())
// Supplier data for KYC profile UUID (by team_uuid)
const suppliersData = ref<Map<string, any>>(new Map())
type SupplierData = NonNullable<GetSupplierProfileByTeamQueryResult['getSupplierProfileByTeam']>
const suppliersData = ref<Map<string, SupplierData>>(new Map())
const summaryTitle = computed(() => `${productName.value}${locationName.value}`)
const summaryMeta = computed(() => {
@@ -149,14 +152,16 @@ const fetchOffersByHub = async () => {
const offers = offersResponse?.nearestOffers || []
// Offers already include routes from backend
const offersWithRoutes = offers.map((offer: any) => ({
sourceUuid: offer.uuid,
sourceName: offer.productName,
sourceLat: offer.latitude,
sourceLon: offer.longitude,
distanceKm: offer.distanceKm,
routes: offer.routes || []
}))
const offersWithRoutes = offers
.filter((offer): offer is NonNullable<OfferWithRouteType> => offer !== null)
.map((offer) => ({
sourceUuid: offer.uuid,
sourceName: offer.productName,
sourceLat: offer.latitude,
sourceLon: offer.longitude,
distanceKm: offer.distanceKm,
routes: offer.routes || []
}))
return { offersByHub: offersWithRoutes }
}
@@ -198,10 +203,12 @@ const mapRouteStages = (route: RoutePathType): RouteStageItem[] => {
const getRouteStages = (option: ProductRouteOption) => {
const route = option.routes?.[0]
if (!route?.stages) return []
return route.stages.filter(Boolean).map((stage: any) => ({
transportType: stage?.transportType,
distanceKm: stage?.distanceKm
}))
return route.stages
.filter((stage): stage is NonNullable<RouteStageType> => stage !== null)
.map((stage) => ({
transportType: stage.transportType,
distanceKm: stage.distanceKm
}))
}
// Get offer data for card
@@ -233,8 +240,8 @@ const loadOfferDetails = async (options: ProductRouteOption[]) => {
return
}
const newOffersData = new Map<string, any>()
const newSuppliersData = new Map<string, any>()
const newOffersData = new Map<string, OfferData>()
const newSuppliersData = new Map<string, SupplierData>()
const teamUuidsToLoad = new Set<string>()
// First, load all offers

View File

@@ -35,14 +35,16 @@
</template>
<script setup lang="ts">
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
import { GetProductsDocument, type GetProductsQueryResult } from '~/composables/graphql/public/exchange-generated'
type Product = NonNullable<NonNullable<GetProductsQueryResult['getProducts']>[number]>
const searchStore = useSearchStore()
const { data, pending, error, refresh } = await useServerQuery('products', GetProductsDocument, {}, 'public', 'exchange')
const productsData = computed(() => data.value?.getProducts || [])
const selectProduct = (product: any) => {
const selectProduct = (product: Product) => {
searchStore.setProduct(product.name)
searchStore.setProductUuid(product.uuid)
const locationUuid = searchStore.searchForm.locationUuid

View File

@@ -154,10 +154,44 @@
</template>
<script setup lang="ts">
interface KycSubmitData {
company_name: string
company_full_name: string
inn: string
kpp: string
ogrn: string
address: string
bank_name: string
bik: string
correspondent_account: string
contact_person: string
contact_email: string
contact_phone: string
}
interface CompanySuggestion {
value: string
unrestricted_value: string
data: {
inn: string
kpp?: string
ogrn?: string
address?: { value: string }
}
}
interface BankSuggestion {
value: string
data: {
bic: string
correspondent_account?: string
}
}
const { t } = useI18n()
const emit = defineEmits<{
submit: [data: any]
submit: [data: KycSubmitData]
}>()
const loading = ref(false)
@@ -195,7 +229,7 @@ const isFormValid = computed(() => {
})
// Handlers
const onCompanySelect = (company: any) => {
const onCompanySelect = (company: CompanySuggestion) => {
formData.value.company = {
companyName: company.value,
companyFullName: company.unrestricted_value,
@@ -206,7 +240,7 @@ const onCompanySelect = (company: any) => {
}
}
const onBankSelect = (bank: any) => {
const onBankSelect = (bank: BankSuggestion) => {
formData.value.bank = {
bankName: bank.value,
bik: bank.data.bic,

View File

@@ -16,8 +16,8 @@
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<Card
v-for="addr in teamAddresses"
:key="addr.uuid"
v-for="(addr, index) in teamAddresses"
:key="addr.uuid ?? index"
padding="small"
interactive
@click="selectTeamAddress(addr)"
@@ -57,8 +57,8 @@
<Grid v-else :cols="1" :md="2" :lg="3" :gap="4">
<HubCard
v-for="location in locationsData"
:key="location.uuid"
v-for="(location, index) in locationsData"
:key="location.uuid ?? index"
:hub="location"
selectable
@select="selectLocation(location)"
@@ -69,7 +69,17 @@
</template>
<script setup lang="ts">
import { HubsListDocument } from '~/composables/graphql/public/geo-generated'
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()
@@ -85,35 +95,37 @@ const calculateDistance = (lat: number, lng: number) => {
// Load logistics hubs
const { data: locationsDataRaw, pending, error, refresh } = await useServerQuery('locations', HubsListDocument, { limit: 100 }, 'public', 'geo')
const locationsData = computed(() => {
return (locationsDataRaw.value?.hubsList || []).map((location: any) => ({
...location,
distance: location?.latitude && location?.longitude
? calculateDistance(location.latitude, location.longitude)
: undefined,
}))
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<any[]>([])
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 || []
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: any) => {
const selectLocation = (location: HubWithDistance) => {
searchStore.setLocation(location.name)
searchStore.setLocationUuid(location.uuid)
history.back()
}
const selectTeamAddress = (addr: any) => {
const selectTeamAddress = (addr: TeamAddress) => {
searchStore.setLocation(addr.address)
searchStore.setLocationUuid(addr.uuid)
history.back()

View File

@@ -52,8 +52,8 @@
<!-- Hubs Tab -->
<div v-else-if="activeTab === 'hubs'" class="space-y-2">
<HubCard
v-for="hub in hubs"
:key="hub.uuid"
v-for="(hub, index) in hubs"
:key="hub.uuid ?? index"
:hub="hub"
selectable
:is-selected="selectedItemId === hub.uuid"
@@ -67,8 +67,8 @@
<!-- Suppliers Tab -->
<div v-else-if="activeTab === 'suppliers'" class="space-y-2">
<SupplierCard
v-for="supplier in suppliers"
:key="supplier.uuid"
v-for="(supplier, index) in suppliers"
:key="supplier.uuid ?? index"
:supplier="supplier"
selectable
:is-selected="selectedItemId === supplier.uuid"
@@ -82,8 +82,8 @@
<!-- Offers Tab -->
<div v-else-if="activeTab === 'offers'" class="space-y-2">
<OfferCard
v-for="offer in offers"
:key="offer.uuid"
v-for="(offer, index) in offers"
:key="offer.uuid ?? index"
:offer="offer"
selectable
:is-selected="selectedItemId === offer.uuid"
@@ -98,18 +98,56 @@
</template>
<script setup lang="ts">
interface Hub {
uuid?: string | null
name?: string | null
country?: string | null
countryCode?: string | null
distance?: string
transportTypes?: (string | null)[] | null
}
interface Supplier {
uuid?: string | null
teamUuid?: string | null
name?: string | null
country?: string | null
countryCode?: string | null
logo?: string | null
onTimeRate?: number | null
offersCount?: number | null
isVerified?: boolean | null
}
interface Offer {
uuid?: string | null
productUuid?: string | null
productName?: string | null
categoryName?: string | null
locationUuid?: string | null
locationName?: string | null
locationCountry?: string | null
locationCountryCode?: string | null
quantity?: number | string | null
unit?: string | null
pricePerUnit?: number | string | null
currency?: string | null
status?: string | null
validUntil?: string | null
}
defineProps<{
activeTab: 'hubs' | 'suppliers' | 'offers'
hubs: any[]
suppliers: any[]
offers: any[]
hubs: Hub[]
suppliers: Supplier[]
offers: Offer[]
selectedItemId: string | null
isLoading: boolean
}>()
defineEmits<{
'update:activeTab': [tab: 'hubs' | 'suppliers' | 'offers']
'select': [item: any, type: string]
'select': [item: Hub | Supplier | Offer, type: 'hub' | 'supplier' | 'offer']
}>()
const localePath = useLocalePath()

View File

@@ -4,7 +4,7 @@
<MapboxMap
:key="mapId"
:map-id="mapId"
:style="`height: ${height}px; width: 100%;`"
:style="`height: ${heightValue}px; width: 100%;`"
class="rounded-lg border border-base-300"
:options="mapOptions"
@load="onMapCreated"
@@ -26,16 +26,46 @@ import type { Map as MapboxMapType } from 'mapbox-gl'
import { LngLatBounds, Popup } from 'mapbox-gl'
import { getCurrentInstance } from 'vue'
const props = defineProps({
stages: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 400
}
})
interface StageCompany {
uuid?: string | null
name?: string | null
}
interface StageTrip {
uuid?: string | null
company?: StageCompany | null
}
interface RouteStage {
uuid?: string | null
stageType?: string | null
sourceLatitude?: number | null
sourceLongitude?: number | null
sourceLocationName?: string | null
destinationLatitude?: number | null
destinationLongitude?: number | null
destinationLocationName?: string | null
locationLatitude?: number | null
locationLongitude?: number | null
locationName?: string | null
selectedCompany?: StageCompany | null
trips?: StageTrip[] | null
}
interface RoutePoint {
id: string
name: string
lat: number
lng: number
companies: StageCompany[]
}
const props = defineProps<{
stages?: RouteStage[]
height?: number
}>()
const defaultHeight = 400
const { t } = useI18n()
const mapRef = ref<MapboxMapType | null>(null)
@@ -44,10 +74,12 @@ const didFitBounds = ref(false)
const instanceId = getCurrentInstance()?.uid || Math.floor(Math.random() * 100000)
const mapId = computed(() => `route-map-${instanceId}`)
const routePoints = computed(() => {
const points: Array<{ id: string; name: string; lat: number; lng: number; companies: any[] }> = []
const heightValue = computed(() => props.height ?? defaultHeight)
props.stages.forEach((stage: any) => {
const routePoints = computed(() => {
const points: RoutePoint[] = []
props.stages?.forEach((stage: RouteStage) => {
if (stage.stageType === 'transport') {
if (stage.sourceLatitude && stage.sourceLongitude) {
const existingPoint = points.find(p => p.lat === stage.sourceLatitude && p.lng === stage.sourceLongitude)
@@ -263,16 +295,16 @@ watch(
{ deep: true }
)
const getStageCompanies = (stage: any) => {
const companies: any[] = []
const getStageCompanies = (stage: RouteStage): StageCompany[] => {
const companies: StageCompany[] = []
if (stage.selectedCompany) {
companies.push(stage.selectedCompany)
}
const uniqueCompanies = new Set()
stage.trips?.forEach((trip: any) => {
if (trip.company && !uniqueCompanies.has(trip.company.uuid)) {
const uniqueCompanies = new Set<string>()
stage.trips?.forEach((trip: StageTrip) => {
if (trip.company && trip.company.uuid && !uniqueCompanies.has(trip.company.uuid)) {
uniqueCompanies.add(trip.company.uuid)
companies.push(trip.company)
}

View File

@@ -70,6 +70,7 @@
<script setup lang="ts">
import { CreateTeamDocument } from '~/composables/graphql/user/teams-generated'
const { t } = useI18n()
const emit = defineEmits(['teamCreated', 'cancel'])
const teamName = ref('')
@@ -93,9 +94,9 @@ const handleSubmit = async () => {
emit('teamCreated', result.createTeam?.team)
teamName.value = ''
teamType.value = 'BUYER'
} catch (err: any) {
} catch (err: unknown) {
hasError.value = true
error.value = err?.message || $t('teams.errors.create_failed')
error.value = err instanceof Error ? err.message : t('teams.errors.create_failed')
console.error('Error creating team:', err)
} finally {
isLoading.value = false

View File

@@ -86,7 +86,6 @@ interface Offer {
status?: string | null
latitude?: number | null
longitude?: number | null
lines?: any[] | null
}
interface Supplier {

View File

@@ -61,11 +61,10 @@
<script setup lang="ts">
interface SelectedItem {
uuid: string
name?: string
country?: string
name?: string | null
country?: string | null
latitude?: number | null
longitude?: number | null
[key: string]: any
}
defineProps<{

View File

@@ -35,7 +35,8 @@
<script setup lang="ts">
interface Offer {
uuid: string
[key: string]: any
name?: string | null
productUuid?: string | null
}
defineProps<{

View File

@@ -96,7 +96,7 @@ import type { SelectMode } from '~/composables/useCatalogSearch'
interface Item {
uuid?: string | null
name?: string | null
[key: string]: any
country?: string | null
}
const props = defineProps<{