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

@@ -46,9 +46,15 @@
</template>
<script setup lang="ts">
import { GetOrderDocument } from '~/composables/graphql/team/orders-generated'
import { GetOrderDocument, type GetOrderQueryResult } from '~/composables/graphql/team/orders-generated'
import type { RouteStageItem } from '~/components/RouteStagesList.vue'
// Types from GraphQL
type OrderType = NonNullable<GetOrderQueryResult['getOrder']>
type StageType = NonNullable<NonNullable<OrderType['stages']>[number]>
type TripType = NonNullable<NonNullable<StageType['trips']>[number]>
type CompanyType = NonNullable<StageType['selectedCompany']>
definePageMeta({
layout: 'topnav',
middleware: ['auth-oidc']
@@ -57,7 +63,7 @@ definePageMeta({
const route = useRoute()
const { t } = useI18n()
const order = ref<any>(null)
const order = ref<OrderType | null>(null)
const isLoadingOrder = ref(true)
const hasOrderError = ref(false)
const orderError = ref('')
@@ -96,8 +102,8 @@ const orderMeta = computed(() => {
const orderRoutesForMap = computed(() => {
const stages = (order.value?.stages || [])
.filter(Boolean)
.map((stage: any) => {
.filter((stage): stage is StageType => stage !== null)
.map((stage) => {
if (stage.stageType === 'transport') {
if (!stage.sourceLatitude || !stage.sourceLongitude || !stage.destinationLatitude || !stage.destinationLongitude) return null
return {
@@ -118,33 +124,43 @@ const orderRoutesForMap = computed(() => {
return [{ stages }]
})
// Company summary type
interface CompanySummary {
name: string | null | undefined
totalWeight: number
tripsCount: number
company: CompanyType | null | undefined
}
const orderStageItems = computed<RouteStageItem[]>(() => {
return (order.value?.stages || []).map((stage: any) => {
const isTransport = stage.stageType === 'transport'
const from = isTransport ? stage.sourceLocationName : stage.locationName
const to = isTransport ? stage.destinationLocationName : stage.locationName
return (order.value?.stages || [])
.filter((stage): stage is StageType => stage !== null)
.map((stage) => {
const isTransport = stage.stageType === 'transport'
const from = isTransport ? stage.sourceLocationName : stage.locationName
const to = isTransport ? stage.destinationLocationName : stage.locationName
const meta: string[] = []
const dateRange = getStageDateRange(stage)
if (dateRange) {
meta.push(dateRange)
}
const meta: string[] = []
const dateRange = getStageDateRange(stage)
if (dateRange) {
meta.push(dateRange)
}
const companies = getCompaniesSummary(stage)
companies.forEach((company: any) => {
meta.push(
`${company.name} · ${company.totalWeight || 0}${t('ordersDetail.labels.weight_unit')} · ${company.tripsCount || 0} ${t('ordersDetail.labels.trips')}`
)
const companies = getCompaniesSummary(stage)
companies.forEach((company: CompanySummary) => {
meta.push(
`${company.name} · ${company.totalWeight || 0}${t('ordersDetail.labels.weight_unit')} · ${company.tripsCount || 0} ${t('ordersDetail.labels.trips')}`
)
})
return {
key: stage.uuid ?? undefined,
from: from ?? undefined,
to: to ?? undefined,
label: stage.name ?? undefined,
meta
}
})
return {
key: stage.uuid,
from,
to,
label: stage.name,
meta
}
})
})
const loadOrder = async () => {
@@ -154,10 +170,10 @@ const loadOrder = async () => {
const orderUuid = route.params.id as string
const { data, error: orderErrorResp } = await useServerQuery('order-detail', GetOrderDocument, { orderUuid }, 'team', 'orders')
if (orderErrorResp.value) throw orderErrorResp.value
order.value = data.value?.getOrder
} catch (err: any) {
order.value = data.value?.getOrder ?? null
} catch (err: unknown) {
hasOrderError.value = true
orderError.value = err.message || t('ordersDetail.errors.load_failed')
orderError.value = err instanceof Error ? err.message : t('ordersDetail.errors.load_failed')
} finally {
isLoadingOrder.value = false
}
@@ -172,8 +188,8 @@ const formatPrice = (price: number, currency?: string | null) => {
}).format(price)
}
const getCompaniesSummary = (stage: any) => {
const companies = []
const getCompaniesSummary = (stage: StageType): CompanySummary[] => {
const companies: CompanySummary[] = []
if (stage.stageType === 'service' && stage.selectedCompany) {
companies.push({
name: stage.selectedCompany.name,
@@ -185,12 +201,13 @@ const getCompaniesSummary = (stage: any) => {
}
if (stage.stageType === 'transport' && stage.trips?.length) {
const companiesMap = new Map()
stage.trips.forEach((trip: any) => {
const companiesMap = new Map<string, CompanySummary>()
stage.trips.forEach((trip) => {
if (!trip) return
const companyName = trip.company?.name || t('ordersDetail.labels.company_unknown')
const weight = trip.plannedWeight || 0
if (companiesMap.has(companyName)) {
const existing = companiesMap.get(companyName)
const existing = companiesMap.get(companyName)!
existing.totalWeight += weight
existing.tripsCount += 1
} else {
@@ -211,10 +228,12 @@ const getOrderDuration = () => {
if (!order.value?.stages?.length) return 0
let minDate: Date | null = null
let maxDate: Date | null = null
order.value.stages.forEach((stage: any) => {
stage.trips?.forEach((trip: any) => {
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate)
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate)
order.value.stages.forEach((stage) => {
if (!stage) return
stage.trips?.forEach((trip) => {
if (!trip) return
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate || '')
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate || '')
if (!minDate || startDate < minDate) minDate = startDate
if (!maxDate || endDate > maxDate) maxDate = endDate
})
@@ -224,13 +243,14 @@ const getOrderDuration = () => {
return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
}
const getStageDateRange = (stage: any) => {
const getStageDateRange = (stage: StageType) => {
if (!stage.trips?.length) return t('ordersDetail.labels.dates_undefined')
let minDate: Date | null = null
let maxDate: Date | null = null
stage.trips.forEach((trip: any) => {
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate)
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate)
stage.trips.forEach((trip) => {
if (!trip) return
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate || '')
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate || '')
if (!minDate || startDate < minDate) minDate = startDate
if (!maxDate || endDate > maxDate) maxDate = endDate
})

View File

@@ -100,6 +100,10 @@
<script setup lang="ts">
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
import type { GetTeamOrdersQueryResult } from '~/composables/graphql/team/orders-generated'
type TeamOrder = NonNullable<NonNullable<GetTeamOrdersQueryResult['getTeamOrders']>[number]>
type TeamOrderStage = NonNullable<NonNullable<TeamOrder['stages']>[number]>
definePageMeta({
layout: 'topnav',
@@ -131,19 +135,28 @@ const currentBounds = ref<MapBounds | null>(null)
// List items - one per order
const listItems = computed(() => {
return filteredItems.value.map(order => ({
...order,
uuid: order.uuid,
name: order.name || `#${order.uuid.slice(0, 8)}`,
latitude: order.sourceLatitude,
longitude: order.sourceLongitude,
country: order.sourceLocationName
}))
return filteredItems.value
.filter(order => order.uuid)
.map(order => ({
...order,
uuid: order.uuid,
name: order.name || `#${order.uuid!.slice(0, 8)}`,
latitude: order.sourceLatitude,
longitude: order.sourceLongitude,
country: order.sourceLocationName
}))
})
// Map points - two per order (source + destination)
interface MapPoint {
uuid: string
name: string
latitude: number
longitude: number
}
const mapPoints = computed(() => {
const result: any[] = []
const result: MapPoint[] = []
filteredItems.value.forEach(order => {
// Source point
if (order.sourceLatitude && order.sourceLongitude) {
@@ -202,22 +215,26 @@ const onSearch = () => {
// TODO: Implement search
}
const onSelectOrder = (item: any) => {
selectedOrderId.value = item.uuid
navigateTo(localePath(`/clientarea/orders/${item.uuid}`))
const onSelectOrder = (item: { uuid?: string | null }) => {
if (item.uuid) {
selectedOrderId.value = item.uuid
navigateTo(localePath(`/clientarea/orders/${item.uuid}`))
}
}
await init()
const getOrderStartDate = (order: any) => {
const getOrderStartDate = (order: TeamOrder) => {
if (!order.createdAt) return t('ordersDetail.labels.dates_undefined')
return formatDate(order.createdAt)
}
const getOrderEndDate = (order: any) => {
const getOrderEndDate = (order: TeamOrder) => {
let latestDate: Date | null = null
order.stages?.forEach((stage: any) => {
stage.trips?.forEach((trip: any) => {
order.stages?.forEach((stage) => {
if (!stage) return
stage.trips?.forEach((trip) => {
if (!trip) return
const endDate = trip.actualUnloadingDate || trip.plannedUnloadingDate
if (endDate) {
const date = new Date(endDate)
@@ -236,9 +253,10 @@ const getOrderEndDate = (order: any) => {
return t('ordersDetail.labels.dates_undefined')
}
const getCompletedStages = (order: any) => {
const getCompletedStages = (order: TeamOrder) => {
if (!order.stages?.length) return 0
return order.stages.filter((stage: any) => stage.status === 'completed').length
// Note: StageType doesn't have a status field, count all stages for now
return order.stages.filter((stage): stage is TeamOrderStage => stage !== null).length
}
const formatDate = (date: string) => {

View File

@@ -14,8 +14,8 @@
>
<template #cards>
<Card
v-for="order in filteredItems"
:key="order.uuid"
v-for="(order, index) in filteredItems"
:key="order.uuid ?? index"
padding="small"
interactive
:class="{ 'ring-2 ring-primary': selectedOrderId === order.uuid }"
@@ -24,8 +24,8 @@
<Stack gap="2">
<Stack direction="row" justify="between" align="center">
<Text weight="semibold">#{{ order.name }}</Text>
<Badge :variant="getStatusVariant(order.status)" size="sm">
{{ getStatusText(order.status) }}
<Badge :variant="getStatusVariant(order.status || '')" size="sm">
{{ getStatusText(order.status || '') }}
</Badge>
</Stack>
<Text tone="muted" size="sm" class="truncate">
@@ -74,9 +74,11 @@ await init()
const mapRef = ref<{ flyTo: (orderId: string) => void } | null>(null)
const selectedOrderId = ref<string | null>(null)
const selectOrder = (order: any) => {
selectedOrderId.value = order.uuid
mapRef.value?.flyTo(order.uuid)
const selectOrder = (order: { uuid?: string | null }) => {
if (order.uuid) {
selectedOrderId.value = order.uuid
mapRef.value?.flyTo(order.uuid)
}
}
const onMapSelectOrder = (uuid: string) => {