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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -86,7 +86,6 @@ interface Offer {
|
||||
status?: string | null
|
||||
latitude?: number | null
|
||||
longitude?: number | null
|
||||
lines?: any[] | null
|
||||
}
|
||||
|
||||
interface Supplier {
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
<script setup lang="ts">
|
||||
interface Offer {
|
||||
uuid: string
|
||||
[key: string]: any
|
||||
name?: string | null
|
||||
productUuid?: string | null
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -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<{
|
||||
|
||||
Reference in New Issue
Block a user