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,7 +152,9 @@ const fetchOffersByHub = async () => {
const offers = offersResponse?.nearestOffers || []
// Offers already include routes from backend
const offersWithRoutes = offers.map((offer: any) => ({
const offersWithRoutes = offers
.filter((offer): offer is NonNullable<OfferWithRouteType> => offer !== null)
.map((offer) => ({
sourceUuid: offer.uuid,
sourceName: offer.productName,
sourceLat: offer.latitude,
@@ -198,9 +203,11 @@ 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
}))
}
@@ -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) => ({
const locationsData = computed<HubWithDistance[]>(() => {
return (locationsDataRaw.value?.hubsList || [])
.filter((location): location is HubItem => location !== null)
.map((location) => ({
...location,
distance: location?.latitude && location?.longitude
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<{

View File

@@ -13,9 +13,9 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
Date: { input: any; output: any; }
Date: { input: string; output: string; }
DateTime: { input: string; output: string; }
Decimal: { input: any; output: any; }
Decimal: { input: string; output: string; }
};
export type OfferType = {
@@ -181,14 +181,14 @@ export type GetLocationOffersQueryVariables = Exact<{
}>;
export type GetLocationOffersQueryResult = { __typename?: 'PublicQuery', getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: any, unit: string, pricePerUnit?: any | null, currency: string, description: string, validUntil?: any | null, createdAt: string, updatedAt: string } | null> | null };
export type GetLocationOffersQueryResult = { __typename?: 'PublicQuery', getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: string, unit: string, pricePerUnit?: string | null, currency: string, description: string, validUntil?: string | null, createdAt: string, updatedAt: string } | null> | null };
export type GetOfferQueryVariables = Exact<{
uuid: Scalars['String']['input'];
}>;
export type GetOfferQueryResult = { __typename?: 'PublicQuery', getOffer?: { __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: any, unit: string, pricePerUnit?: any | null, currency: string, description: string, validUntil?: any | null, createdAt: string, updatedAt: string } | null };
export type GetOfferQueryResult = { __typename?: 'PublicQuery', getOffer?: { __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: string, unit: string, pricePerUnit?: string | null, currency: string, description: string, validUntil?: string | null, createdAt: string, updatedAt: string } | null };
export type GetOffersQueryVariables = Exact<{
productUuid?: InputMaybe<Scalars['String']['input']>;
@@ -200,7 +200,7 @@ export type GetOffersQueryVariables = Exact<{
}>;
export type GetOffersQueryResult = { __typename?: 'PublicQuery', getOffersCount?: number | null, getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: any, unit: string, pricePerUnit?: any | null, currency: string, description: string, validUntil?: any | null, createdAt: string, updatedAt: string } | null> | null };
export type GetOffersQueryResult = { __typename?: 'PublicQuery', getOffersCount?: number | null, getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: string, unit: string, pricePerUnit?: string | null, currency: string, description: string, validUntil?: string | null, createdAt: string, updatedAt: string } | null> | null };
export type GetProductQueryVariables = Exact<{
uuid: Scalars['String']['input'];
@@ -214,7 +214,7 @@ export type GetProductOffersQueryVariables = Exact<{
}>;
export type GetProductOffersQueryResult = { __typename?: 'PublicQuery', getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: any, unit: string, pricePerUnit?: any | null, currency: string, description: string, validUntil?: any | null, createdAt: string, updatedAt: string } | null> | null };
export type GetProductOffersQueryResult = { __typename?: 'PublicQuery', getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: string, unit: string, pricePerUnit?: string | null, currency: string, description: string, validUntil?: string | null, createdAt: string, updatedAt: string } | null> | null };
export type GetProductsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -226,7 +226,7 @@ export type GetSupplierOffersQueryVariables = Exact<{
}>;
export type GetSupplierOffersQueryResult = { __typename?: 'PublicQuery', getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: any, unit: string, pricePerUnit?: any | null, currency: string, description: string, validUntil?: any | null, createdAt: string, updatedAt: string } | null> | null };
export type GetSupplierOffersQueryResult = { __typename?: 'PublicQuery', getOffers?: Array<{ __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: string, unit: string, pricePerUnit?: string | null, currency: string, description: string, validUntil?: string | null, createdAt: string, updatedAt: string } | null> | null };
export type GetSupplierProfileQueryVariables = Exact<{
uuid: Scalars['String']['input'];

View File

@@ -13,7 +13,7 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
JSONString: { input: any; output: any; }
JSONString: { input: Record<string, unknown>; output: Record<string, unknown>; }
};
/** Cluster or individual point for map display. */
@@ -448,7 +448,7 @@ export type GetAutoRouteQueryVariables = Exact<{
}>;
export type GetAutoRouteQueryResult = { __typename?: 'Query', autoRoute?: { __typename?: 'RouteType', distanceKm?: number | null, geometry?: any | null } | null };
export type GetAutoRouteQueryResult = { __typename?: 'Query', autoRoute?: { __typename?: 'RouteType', distanceKm?: number | null, geometry?: Record<string, unknown> | null } | null };
export type GetClusteredNodesQueryVariables = Exact<{
west: Scalars['Float']['input'];
@@ -483,7 +483,7 @@ export type GetRailRouteQueryVariables = Exact<{
}>;
export type GetRailRouteQueryResult = { __typename?: 'Query', railRoute?: { __typename?: 'RouteType', distanceKm?: number | null, geometry?: any | null } | null };
export type GetRailRouteQueryResult = { __typename?: 'Query', railRoute?: { __typename?: 'RouteType', distanceKm?: number | null, geometry?: Record<string, unknown> | null } | null };
export type HubsListQueryVariables = Exact<{
limit?: InputMaybe<Scalars['Int']['input']>;

View File

@@ -13,10 +13,10 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
Date: { input: any; output: any; }
Date: { input: string; output: string; }
DateTime: { input: string; output: string; }
Decimal: { input: any; output: any; }
JSONString: { input: any; output: any; }
Decimal: { input: string; output: string; }
JSONString: { input: Record<string, unknown>; output: Record<string, unknown>; }
};
export type CreateOffer = {
@@ -205,14 +205,14 @@ export type CreateRequestMutationVariables = Exact<{
}>;
export type CreateRequestMutationResult = { __typename?: 'TeamMutation', createRequest?: { __typename?: 'CreateRequest', request?: { __typename?: 'RequestType', uuid: string, productUuid: string, quantity: any, sourceLocationUuid: string, userId: string } | null } | null };
export type CreateRequestMutationResult = { __typename?: 'TeamMutation', createRequest?: { __typename?: 'CreateRequest', request?: { __typename?: 'RequestType', uuid: string, productUuid: string, quantity: string, sourceLocationUuid: string, userId: string } | null } | null };
export type GetRequestsQueryVariables = Exact<{
userId: Scalars['String']['input'];
}>;
export type GetRequestsQueryResult = { __typename?: 'TeamQuery', getRequests?: Array<{ __typename?: 'RequestType', uuid: string, productUuid: string, quantity: any, sourceLocationUuid: string, userId: string } | null> | null };
export type GetRequestsQueryResult = { __typename?: 'TeamQuery', getRequests?: Array<{ __typename?: 'RequestType', uuid: string, productUuid: string, quantity: string, sourceLocationUuid: string, userId: string } | null> | null };
export const CreateOfferDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOffer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OfferInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOffer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"workflowId"}},{"kind":"Field","name":{"kind":"Name","value":"offerUuid"}}]}}]}}]} as unknown as DocumentNode<CreateOfferMutationResult, CreateOfferMutationVariables>;

View File

@@ -14,7 +14,7 @@ export type Scalars = {
Int: { input: number; output: number; }
Float: { input: number; output: number; }
DateTime: { input: string; output: string; }
JSONString: { input: any; output: any; }
JSONString: { input: Record<string, unknown>; output: Record<string, unknown>; }
};
/** Create KYC Application for Russian company. */
@@ -110,19 +110,19 @@ export type CreateKycApplicationRussiaMutationVariables = Exact<{
}>;
export type CreateKycApplicationRussiaMutationResult = { __typename?: 'UserMutation', createKycApplicationRussia?: { __typename?: 'CreateKYCApplicationRussia', success?: boolean | null, kycApplication?: { __typename?: 'KYCApplicationType', uuid: string, contactEmail: string, createdAt: string, countryData?: any | null } | null } | null };
export type CreateKycApplicationRussiaMutationResult = { __typename?: 'UserMutation', createKycApplicationRussia?: { __typename?: 'CreateKYCApplicationRussia', success?: boolean | null, kycApplication?: { __typename?: 'KYCApplicationType', uuid: string, contactEmail: string, createdAt: string, countryData?: Record<string, unknown> | null } | null } | null };
export type GetKycRequestRussiaQueryVariables = Exact<{
uuid: Scalars['String']['input'];
}>;
export type GetKycRequestRussiaQueryResult = { __typename?: 'UserQuery', kycRequest?: { __typename?: 'KYCApplicationType', uuid: string, userId: string, teamName: string, countryCode: string, contactPerson: string, contactEmail: string, contactPhone: string, approvedBy?: string | null, approvedAt?: string | null, createdAt: string, updatedAt: string, countryData?: any | null } | null };
export type GetKycRequestRussiaQueryResult = { __typename?: 'UserQuery', kycRequest?: { __typename?: 'KYCApplicationType', uuid: string, userId: string, teamName: string, countryCode: string, contactPerson: string, contactEmail: string, contactPhone: string, approvedBy?: string | null, approvedAt?: string | null, createdAt: string, updatedAt: string, countryData?: Record<string, unknown> | null } | null };
export type GetKycRequestsRussiaQueryVariables = Exact<{ [key: string]: never; }>;
export type GetKycRequestsRussiaQueryResult = { __typename?: 'UserQuery', kycRequests?: Array<{ __typename?: 'KYCApplicationType', uuid: string, userId: string, teamName: string, countryCode: string, contactPerson: string, contactEmail: string, contactPhone: string, approvedBy?: string | null, approvedAt?: string | null, createdAt: string, updatedAt: string, countryData?: any | null } | null> | null };
export type GetKycRequestsRussiaQueryResult = { __typename?: 'UserQuery', kycRequests?: Array<{ __typename?: 'KYCApplicationType', uuid: string, userId: string, teamName: string, countryCode: string, contactPerson: string, contactEmail: string, contactPhone: string, approvedBy?: string | null, approvedAt?: string | null, createdAt: string, updatedAt: string, countryData?: Record<string, unknown> | null } | null> | null };
export const CreateKycApplicationRussiaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateKYCApplicationRussia"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"KYCApplicationRussiaInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createKycApplicationRussia"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"kycApplication"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"contactEmail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"countryData"}}]}}]}}]}}]} as unknown as DocumentNode<CreateKycApplicationRussiaMutationResult, CreateKycApplicationRussiaMutationVariables>;

View File

@@ -45,7 +45,7 @@ export function useCatalogHubs() {
)
const itemsByCountry = computed(() => {
const grouped = new Map<string, any[]>()
const grouped = new Map<string, Array<HubItem | NearestHubItem>>()
items.value.forEach(hub => {
const country = hub.country || t('catalogMap.labels.country_unknown')
if (!grouped.has(country)) grouped.set(country, [])

View File

@@ -1,4 +1,4 @@
import type { ProductsListQueryResult } from '~/composables/graphql/public/geo-generated'
import type { ProductsListQueryResult, NearestOffersQueryResult } from '~/composables/graphql/public/geo-generated'
import {
ProductsListDocument,
GetNodeDocument,
@@ -10,6 +10,14 @@ import {
// Type from codegen
type ProductItem = NonNullable<NonNullable<ProductsListQueryResult['productsList']>[number]>
type OfferItem = NonNullable<NonNullable<NearestOffersQueryResult['nearestOffers']>[number]>
// Product aggregated from offers
interface AggregatedProduct {
uuid: string
name: string | null | undefined
offersCount: number
}
// Shared state
const items = ref<ProductItem[]>([])
@@ -61,9 +69,9 @@ export function useCatalogProducts() {
)
// Group offers by product
const productsMap = new Map<string, any>()
offersData?.nearestOffers?.forEach((offer: any) => {
if (offer?.productUuid) {
const productsMap = new Map<string, AggregatedProduct>()
offersData?.nearestOffers?.forEach((offer) => {
if (!offer?.productUuid) return
if (!productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, {
uuid: offer.productUuid,
@@ -72,9 +80,8 @@ export function useCatalogProducts() {
})
}
productsMap.get(offer.productUuid)!.offersCount++
}
})
items.value = Array.from(productsMap.values())
items.value = Array.from(productsMap.values()) as ProductItem[]
}
} else if (filterHubUuid.value) {
// Products near hub - get hub coordinates first
@@ -103,9 +110,9 @@ export function useCatalogProducts() {
)
// Group offers by product
const productsMap = new Map<string, any>()
offersData?.nearestOffers?.forEach((offer: any) => {
if (offer?.productUuid) {
const productsMap = new Map<string, AggregatedProduct>()
offersData?.nearestOffers?.forEach((offer) => {
if (!offer?.productUuid) return
if (!productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, {
uuid: offer.productUuid,
@@ -114,9 +121,8 @@ export function useCatalogProducts() {
})
}
productsMap.get(offer.productUuid)!.offersCount++
}
})
items.value = Array.from(productsMap.values())
items.value = Array.from(productsMap.values()) as ProductItem[]
}
} else {
// All products from graph

View File

@@ -1,4 +1,8 @@
const items = ref<any[]>([])
import type { GetTeamAddressesQueryResult } from '~/composables/graphql/team/teams-generated'
type TeamAddress = NonNullable<NonNullable<GetTeamAddressesQueryResult['teamAddresses']>[number]>
const items = ref<TeamAddress[]>([])
const isLoading = ref(false)
const isInitialized = ref(false)
@@ -23,7 +27,7 @@ export function useTeamAddresses() {
try {
const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated')
const data = await execute(GetTeamAddressesDocument, {}, 'team', 'teams')
items.value = data?.teamAddresses || []
items.value = (data?.teamAddresses || []).filter((a): a is TeamAddress => a !== null)
isInitialized.value = true
} catch (e) {
console.error('Failed to load addresses', e)

View File

@@ -1,4 +1,9 @@
const items = ref<any[]>([])
import type { GetTeamOrdersQueryResult } from '~/composables/graphql/team/orders-generated'
type TeamOrder = NonNullable<NonNullable<GetTeamOrdersQueryResult['getTeamOrders']>[number]>
type TeamOrderStage = NonNullable<NonNullable<TeamOrder['stages']>[number]>
const items = ref<TeamOrder[]>([])
const isLoading = ref(false)
const isInitialized = ref(false)
@@ -23,13 +28,14 @@ export function useTeamOrders() {
const routesForMap = computed(() =>
filteredItems.value
.filter(order => order.uuid && order.name)
.map(order => ({
uuid: order.uuid,
name: order.name,
status: order.status,
uuid: order.uuid!,
name: order.name!,
status: order.status ?? undefined,
stages: (order.stages || [])
.filter((s: any) => s.stageType === 'transport' && s.sourceLatitude && s.sourceLongitude && s.destinationLatitude && s.destinationLongitude)
.map((s: any) => ({
.filter((s): s is TeamOrderStage => s !== null && s.stageType === 'transport' && !!s.sourceLatitude && !!s.sourceLongitude && !!s.destinationLatitude && !!s.destinationLongitude)
.map((s) => ({
fromLat: s.sourceLatitude,
fromLon: s.sourceLongitude,
toLat: s.destinationLatitude,
@@ -47,7 +53,7 @@ export function useTeamOrders() {
try {
const { GetTeamOrdersDocument } = await import('~/composables/graphql/team/orders-generated')
const data = await execute(GetTeamOrdersDocument, {}, 'team', 'orders')
items.value = data?.getTeamOrders || []
items.value = (data?.getTeamOrders || []).filter((o): o is TeamOrder => o !== null)
isInitialized.value = true
} catch (e) {
console.error('Failed to load orders', e)

View File

@@ -113,12 +113,20 @@ const {
const theme = useState<'cupcake' | 'night'>('theme', () => 'cupcake')
// User data state (shared across layouts)
interface SelectedLocation {
type: string
uuid: string
name: string
latitude: number
longitude: number
}
const userData = useState<{
id?: string
firstName?: string
lastName?: string
avatarId?: string
activeTeam?: { name?: string; teamType?: string; logtoOrgId?: string; selectedLocation?: any }
activeTeam?: { name?: string; teamType?: string; logtoOrgId?: string; selectedLocation?: SelectedLocation | null }
activeTeamId?: string
teams?: Array<{ id?: string; name?: string; logtoOrgId?: string }>
} | null>('me', () => null)

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

View File

@@ -94,6 +94,10 @@
import { NuxtLink } from '#components'
import type { MapMouseEvent, Map as MapboxMapType } from 'mapbox-gl'
interface MapboxSearchBox {
value: string
}
definePageMeta({
layout: 'topnav',
middleware: ['auth-oidc']
@@ -112,7 +116,7 @@ const isSaving = ref(false)
const isDeleting = ref(false)
const searchBoxContainer = ref<HTMLElement | null>(null)
const mapInstance = ref<MapboxMapType | null>(null)
const searchBoxRef = ref<any>(null)
const searchBoxRef = ref<MapboxSearchBox | null>(null)
const addressData = ref<{
uuid: string
@@ -130,7 +134,7 @@ const loadAddress = async () => {
const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated')
const data = await execute(GetTeamAddressesDocument, {}, 'team', 'teams')
const addresses = data?.teamAddresses || []
const found = addresses.find((a: any) => a.uuid === uuid.value)
const found = addresses.find((a) => a?.uuid === uuid.value)
if (found) {
addressData.value = {
@@ -167,7 +171,7 @@ const reverseGeocode = async (lat: number, lng: number): Promise<{ address: stri
if (!feature) return { address: null, countryCode: null }
// Extract country code from context
const countryContext = feature.context?.find((c: any) => c.id?.startsWith('country.'))
const countryContext = feature.context?.find((c: { id?: string }) => c.id?.startsWith('country.'))
const countryCode = countryContext?.short_code?.toUpperCase() || null
return { address: feature.place_name, countryCode }
@@ -215,7 +219,7 @@ onMounted(async () => {
searchBox.value = addressData.value.address
}
searchBox.addEventListener('retrieve', (event: any) => {
searchBox.addEventListener('retrieve', (event: CustomEvent) => {
if (!addressData.value) return
const feature = event.detail.features?.[0]

View File

@@ -118,8 +118,10 @@ const onSearch = () => {
// TODO: Implement search
}
const onSelectAddress = (item: any) => {
const onSelectAddress = (item: { uuid?: string | null }) => {
if (item.uuid) {
selectedAddressId.value = item.uuid
}
}
await init()

View File

@@ -62,11 +62,13 @@ await init()
const mapRef = ref<{ flyTo: (lat: number, lng: number, zoom?: number) => void } | null>(null)
const selectedItemId = ref<string | null>(null)
const selectItem = (item: any) => {
const selectItem = (item: { uuid?: string | null; latitude?: number | null; longitude?: number | null }) => {
if (item.uuid) {
selectedItemId.value = item.uuid
if (item.latitude && item.longitude) {
mapRef.value?.flyTo(item.latitude, item.longitude, 8)
}
}
}
const onMapSelectItem = (uuid: string) => {

View File

@@ -71,6 +71,10 @@
import { NuxtLink } from '#components'
import type { MapMouseEvent, Map as MapboxMapType } from 'mapbox-gl'
interface MapboxSearchBox {
value: string
}
definePageMeta({
layout: 'topnav',
middleware: ['auth-oidc']
@@ -84,7 +88,7 @@ const config = useRuntimeConfig()
const isCreating = ref(false)
const searchBoxContainer = ref<HTMLElement | null>(null)
const mapInstance = ref<MapboxMapType | null>(null)
const searchBoxRef = ref<any>(null)
const searchBoxRef = ref<MapboxSearchBox | null>(null)
const newAddress = reactive({
name: '',
@@ -110,7 +114,7 @@ const reverseGeocode = async (lat: number, lng: number): Promise<{ address: stri
if (!feature) return { address: null, countryCode: null }
// Extract country code from context
const countryContext = feature.context?.find((c: any) => c.id?.startsWith('country.'))
const countryContext = feature.context?.find((c: { id?: string }) => c.id?.startsWith('country.'))
const countryCode = countryContext?.short_code?.toUpperCase() || null
return { address: feature.place_name, countryCode }
@@ -151,7 +155,7 @@ onMounted(async () => {
}
searchBox.placeholder = t('profileAddresses.form.address.placeholder')
searchBox.addEventListener('retrieve', (event: any) => {
searchBox.addEventListener('retrieve', (event: CustomEvent) => {
const feature = event.detail.features?.[0]
if (feature) {
const [lng, lat] = feature.geometry.coordinates

View File

@@ -109,9 +109,9 @@ const handleSend = async () => {
const content = last?.content?.[0]?.text || last?.content || t('aiAssistants.view.emptyResponse')
chat.value.push({ role: 'assistant', content })
scrollToBottom()
} catch (e: any) {
} catch (e: unknown) {
console.error('Agent error', e)
error.value = e?.message || t('aiAssistants.view.error')
error.value = e instanceof Error ? e.message : t('aiAssistants.view.error')
chat.value.push({ role: 'assistant', content: t('aiAssistants.view.error') })
scrollToBottom()
} finally {

View File

@@ -95,6 +95,10 @@
</template>
<script setup lang="ts">
import type { GetTeamTransactionsQueryResult } from '~/composables/graphql/team/billing-generated'
type Transaction = NonNullable<NonNullable<GetTeamTransactionsQueryResult['teamTransactions']>[number]>
definePageMeta({
layout: 'topnav',
middleware: ['auth-oidc']
@@ -112,7 +116,7 @@ const balance = ref({
exists: false
})
const transactions = ref<any[]>([])
const transactions = ref<Transaction[]>([])
const formatCurrency = (amount: number) => {
// Amount is in kopecks, convert to base units
@@ -130,7 +134,7 @@ const formatAmount = (amount: number) => {
}).format(amount / 100)
}
const formatTimestamp = (timestamp: number) => {
const formatTimestamp = (timestamp: number | null | undefined) => {
if (!timestamp) return '—'
// TigerBeetle timestamp is in nanoseconds since epoch
const date = new Date(timestamp / 1000000)
@@ -157,8 +161,8 @@ const loadBalance = async () => {
if (data.value?.teamBalance) {
balance.value = data.value.teamBalance
}
} catch (e: any) {
error.value = e.message || t('billing.errors.load_failed')
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : t('billing.errors.load_failed')
} finally {
isLoading.value = false
}
@@ -171,7 +175,7 @@ const loadTransactions = async () => {
if (txError.value) throw txError.value
transactions.value = data.value?.teamTransactions || []
transactions.value = (data.value?.teamTransactions || []).filter((tx): tx is Transaction => tx !== null)
} catch (e) {
console.error('Failed to load transactions', e)
}

View File

@@ -146,8 +146,8 @@ const switchToTeam = async (teamId: string) => {
markActiveTeam(newActiveId)
navigateTo(localePath('/clientarea/team'))
}
} catch (err: any) {
error.value = err.message || t('clientTeamSwitch.error.switch')
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : t('clientTeamSwitch.error.switch')
hasError.value = true
}
}

View File

@@ -24,7 +24,7 @@
<Card v-for="request in kycRequests" :key="request.uuid" padding="lg">
<Stack gap="3">
<Stack direction="row" gap="2" align="center" justify="between">
<Heading :level="4" weight="semibold">{{ request.companyName || t('kycOverview.list.unnamed') }}</Heading>
<Heading :level="4" weight="semibold">{{ request.teamName || t('kycOverview.list.unnamed') }}</Heading>
<Pill :variant="getStatusVariant(request)" :tone="getStatusTone(request)">
{{ getStatusText(request) }}
</Pill>
@@ -32,8 +32,8 @@
<Text tone="muted" size="base">
{{ t('kycOverview.list.submitted') }}: {{ formatDate(request.createdAt) }}
</Text>
<Text v-if="request.inn" tone="muted" size="base">
{{ t('kycOverview.list.inn') }}: {{ request.inn }}
<Text tone="muted" size="base">
{{ t('kycOverview.list.country') }}: {{ request.countryCode }}
</Text>
</Stack>
</Card>
@@ -91,7 +91,9 @@
</template>
<script setup lang="ts">
import { GetKycRequestsRussiaDocument } from '~/composables/graphql/user/kyc-generated'
import { GetKycRequestsRussiaDocument, type GetKycRequestsRussiaQueryResult } from '~/composables/graphql/user/kyc-generated'
type KycRequest = NonNullable<NonNullable<GetKycRequestsRussiaQueryResult['kycRequests']>[number]>
definePageMeta({
layout: 'topnav',
@@ -102,7 +104,7 @@ const { t } = useI18n()
const loading = ref(true)
const error = ref<string | null>(null)
const kycRequests = ref<any[]>([])
const kycRequests = ref<KycRequest[]>([])
const selectCountry = (country: string) => {
if (country === 'russia') {
@@ -110,21 +112,18 @@ const selectCountry = (country: string) => {
}
}
const getStatusVariant = (request: any) => {
const getStatusVariant = (request: KycRequest) => {
if (request.approvedAt) return 'primary'
if (request.rejectedAt) return 'outline'
return 'outline'
}
const getStatusTone = (request: any) => {
const getStatusTone = (request: KycRequest) => {
if (request.approvedAt) return 'success'
if (request.rejectedAt) return 'error'
return 'warning'
}
const getStatusText = (request: any) => {
const getStatusText = (request: KycRequest) => {
if (request.approvedAt) return t('kycOverview.list.status.approved')
if (request.rejectedAt) return t('kycOverview.list.status.rejected')
return t('kycOverview.list.status.pending')
}
@@ -143,10 +142,10 @@ const loadKYCStatus = async () => {
if (kycError.value) throw kycError.value
const requests = data.value?.kycRequests || []
// Сортируем по дате создания (новые первые)
kycRequests.value = [...requests].sort((a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
} catch (err: any) {
kycRequests.value = [...requests]
.filter((r): r is KycRequest => r !== null)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
} catch (err: unknown) {
error.value = t('kycOverview.errors.load_failed')
} finally {
loading.value = false

View File

@@ -57,24 +57,39 @@ const submitting = ref(false)
const submitError = ref<string | null>(null)
const submitSuccess = ref(false)
const handleSubmit = async (formData: any) => {
interface KycFormData {
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
}
const handleSubmit = async (formData: KycFormData) => {
try {
submitting.value = true
submitError.value = null
const submitData = {
companyName: formData.company_name,
companyFullName: formData.company_full_name,
inn: formData.inn,
companyName: formData.company_name || '',
companyFullName: formData.company_full_name || '',
inn: formData.inn || '',
kpp: formData.kpp || '',
ogrn: formData.ogrn || '',
address: formData.address,
bankName: formData.bank_name,
bik: formData.bik,
address: formData.address || '',
bankName: formData.bank_name || '',
bik: formData.bik || '',
correspondentAccount: formData.correspondent_account || '',
contactPerson: formData.contact_person,
contactEmail: formData.contact_email,
contactPhone: formData.contact_phone,
contactPerson: formData.contact_person || '',
contactEmail: formData.contact_email || '',
contactPhone: formData.contact_phone || '',
}
const result = await mutate(CreateKycApplicationRussiaDocument, { input: submitData }, 'user', 'kyc')
@@ -85,8 +100,8 @@ const handleSubmit = async (formData: any) => {
} else {
throw new Error(t('kycRussia.errors.create_failed'))
}
} catch (err: any) {
submitError.value = err.message || t('kycRussia.errors.submit_failed')
} catch (err: unknown) {
submitError.value = err instanceof Error ? err.message : t('kycRussia.errors.submit_failed')
} finally {
submitting.value = false
}

View File

@@ -118,7 +118,9 @@ import { FormKitSchema } from '@formkit/vue'
import type { FormKitSchemaNode } from '@formkit/core'
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
import { CreateOfferDocument } from '~/composables/graphql/team/exchange-generated'
import { GetTeamAddressesDocument } from '~/composables/graphql/team/teams-generated'
import { GetTeamAddressesDocument, type GetTeamAddressesQueryResult } from '~/composables/graphql/team/teams-generated'
type TeamAddress = NonNullable<NonNullable<GetTeamAddressesQueryResult['teamAddresses']>[number]>
definePageMeta({
layout: 'topnav',
@@ -147,7 +149,7 @@ const productName = ref<string>('')
const schemaId = ref<string | null>(null)
const schemaDescription = ref<string | null>(null)
const formkitSchema = ref<FormKitSchemaNode[]>([])
const addresses = ref<any[]>([])
const addresses = ref<TeamAddress[]>([])
const selectedAddressUuid = ref<string | null>(null)
const formKitConfig = {
classes: {
@@ -169,8 +171,8 @@ const loadAddresses = async () => {
try {
const { data, error: addressesError } = await useServerQuery('offer-form-addresses', GetTeamAddressesDocument, {}, 'team', 'teams')
if (addressesError.value) throw addressesError.value
addresses.value = data.value?.teamAddresses || []
const defaultAddress = addresses.value.find((address: any) => address.isDefault)
addresses.value = (data.value?.teamAddresses || []).filter((a): a is TeamAddress => a !== null)
const defaultAddress = addresses.value.find((address) => address.isDefault)
selectedAddressUuid.value = defaultAddress?.uuid || addresses.value[0]?.uuid || null
} catch (err) {
console.error('Failed to load addresses:', err)
@@ -189,7 +191,7 @@ const loadData = async () => {
const { data: productsData, error: productsError } = await useServerQuery('offer-form-products', GetProductsDocument, {}, 'public', 'exchange')
if (productsError.value) throw productsError.value
const products = productsData.value?.getProducts || []
const product = products.find((p: any) => p.uuid === productUuid.value)
const product = products.find((p) => p?.uuid === productUuid.value)
if (!product) {
throw new Error(t('clientOfferForm.errors.productNotFound', { uuid: productUuid.value }))
@@ -219,9 +221,9 @@ const loadData = async () => {
formkitSchema.value = schemaToFormKit(terminusClass, enums)
await loadAddresses()
} catch (err: any) {
} catch (err: unknown) {
hasError.value = true
error.value = err.message || t('clientOfferForm.error.load')
error.value = err instanceof Error ? err.message : t('clientOfferForm.error.load')
console.error('Load error:', err)
} finally {
isLoading.value = false
@@ -237,7 +239,7 @@ const handleSubmit = async (data: Record<string, unknown>) => {
throw new Error(t('clientOfferForm.error.load'))
}
const selectedAddress = addresses.value.find((address: any) => address.uuid === selectedAddressUuid.value)
const selectedAddress = addresses.value.find((address) => address?.uuid === selectedAddressUuid.value)
if (!selectedAddress) {
throw new Error(t('clientOfferForm.error.save'))
}
@@ -253,14 +255,14 @@ const handleSubmit = async (data: Record<string, unknown>) => {
locationCountryCode: selectedAddress.countryCode || '',
locationLatitude: selectedAddress.latitude,
locationLongitude: selectedAddress.longitude,
quantity: data.quantity || 0,
quantity: String(data.quantity || '0'),
unit: String(data.unit || 'ton'),
pricePerUnit: data.price_per_unit || data.pricePerUnit || null,
pricePerUnit: String(data.price_per_unit || data.pricePerUnit || ''),
currency: String(data.currency || 'USD'),
description: String(data.description || ''),
validUntil: data.valid_until || data.validUntil || null,
validUntil: (data.valid_until as string | undefined) ?? (data.validUntil as string | undefined) ?? undefined,
terminusSchemaId: schemaId.value,
terminusPayload: JSON.stringify(data),
terminusPayload: data,
}
const result = await mutate(CreateOfferDocument, { input }, 'team', 'exchange')
@@ -270,8 +272,8 @@ const handleSubmit = async (data: Record<string, unknown>) => {
await navigateTo(localePath('/clientarea/offers'))
} catch (err: any) {
error.value = err.message || t('clientOfferForm.error.save')
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : t('clientOfferForm.error.save')
hasError.value = true
} finally {
isSubmitting.value = false

View File

@@ -122,7 +122,9 @@
<script setup lang="ts">
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
import { GetOffersDocument } from '~/composables/graphql/public/exchange-generated'
import { GetOffersDocument, type GetOffersQueryResult } from '~/composables/graphql/public/exchange-generated'
type Offer = NonNullable<NonNullable<GetOffersQueryResult['getOffers']>[number]>
definePageMeta({
layout: 'topnav',
@@ -135,7 +137,7 @@ const { activeTeamId } = useActiveTeam()
const { execute } = useGraphQL()
const PAGE_SIZE = 24
const offers = ref<any[]>([])
const offers = ref<Offer[]>([])
const totalOffers = ref(0)
const isLoadingMore = ref(false)
@@ -164,7 +166,7 @@ const {
watchEffect(() => {
if (offersData.value?.getOffers) {
offers.value = offersData.value.getOffers
offers.value = offersData.value.getOffers.filter((o): o is Offer => o !== null)
totalOffers.value = offersData.value.getOffersCount ?? offersData.value.getOffers.length
}
})
@@ -231,9 +233,11 @@ const onSearch = () => {
// TODO: Implement search
}
const onSelectOffer = (offer: any) => {
const onSelectOffer = (offer: { uuid?: string | null }) => {
if (offer.uuid) {
selectedOfferId.value = offer.uuid
navigateTo(localePath(`/clientarea/offers/${offer.uuid}`))
}
}
const getStatusVariant = (status: string) => {
@@ -293,7 +297,7 @@ const fetchOffers = async (offset = 0, replace = false) => {
'public',
'exchange'
)
const next = data?.getOffers || []
const next = (data?.getOffers || []).filter((o): o is Offer => o !== null)
offers.value = replace ? next : offers.value.concat(next)
totalOffers.value = data?.getOffersCount ?? totalOffers.value
}

View File

@@ -26,8 +26,8 @@
<template v-else>
<Grid v-if="products.length" :cols="1" :md="2" :lg="3" :gap="4">
<Card
v-for="product in products"
:key="product.uuid"
v-for="(product, index) in products"
:key="product.uuid ?? index"
padding="lg"
class="cursor-pointer hover:shadow-md transition-shadow"
@click="selectProduct(product)"
@@ -51,7 +51,9 @@
</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]>
definePageMeta({
layout: 'topnav',
@@ -62,7 +64,7 @@ const localePath = useLocalePath()
const { t } = useI18n()
const { execute } = useGraphQL()
const products = ref<any[]>([])
const products = ref<Product[]>([])
const isLoading = ref(true)
const hasError = ref(false)
const error = ref('')
@@ -73,19 +75,19 @@ const loadProducts = async () => {
hasError.value = false
const { data, error: productsError } = await useServerQuery('offers-new-products', GetProductsDocument, {}, 'public', 'exchange')
if (productsError.value) throw productsError.value
products.value = data.value?.getProducts || []
} catch (err: any) {
products.value = (data.value?.getProducts || []).filter((p): p is Product => p !== null)
} catch (err: unknown) {
hasError.value = true
error.value = err.message || t('offersNew.errors.load_failed')
error.value = err instanceof Error ? err.message : t('offersNew.errors.load_failed')
products.value = []
} finally {
isLoading.value = false
}
}
const selectProduct = (product: any) => {
const selectProduct = (product: { uuid?: string | null }) => {
// Navigate to product details page
navigateTo(localePath(`/clientarea/offers/${product.uuid}`))
if (product.uuid) navigateTo(localePath(`/clientarea/offers/${product.uuid}`))
}
await loadProducts()

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,8 +124,18 @@ 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) => {
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
@@ -131,17 +147,17 @@ const orderStageItems = computed<RouteStageItem[]>(() => {
}
const companies = getCompaniesSummary(stage)
companies.forEach((company: any) => {
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,
from,
to,
label: stage.name,
key: stage.uuid ?? undefined,
from: from ?? undefined,
to: to ?? undefined,
label: stage.name ?? undefined,
meta
}
})
@@ -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,10 +135,12 @@ const currentBounds = ref<MapBounds | null>(null)
// List items - one per order
const listItems = computed(() => {
return filteredItems.value.map(order => ({
return filteredItems.value
.filter(order => order.uuid)
.map(order => ({
...order,
uuid: order.uuid,
name: order.name || `#${order.uuid.slice(0, 8)}`,
name: order.name || `#${order.uuid!.slice(0, 8)}`,
latitude: order.sourceLatitude,
longitude: order.sourceLongitude,
country: order.sourceLocationName
@@ -142,8 +148,15 @@ const listItems = computed(() => {
})
// 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) => {
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) => {
const selectOrder = (order: { uuid?: string | null }) => {
if (order.uuid) {
selectedOrderId.value = order.uuid
mapRef.value?.flyTo(order.uuid)
}
}
const onMapSelectOrder = (uuid: string) => {

View File

@@ -48,8 +48,8 @@
<Heading :level="2">{{ t('clientTeam.members.title') }}</Heading>
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<Card
v-for="member in currentTeam?.members || []"
:key="member.user?.id"
v-for="(member, index) in currentTeamMembers"
:key="member.user?.id ?? `member-${index}`"
padding="lg"
>
<Stack gap="3">
@@ -67,7 +67,7 @@
<!-- Pending invitations -->
<Card
v-for="invitation in currentTeam?.invitations || []"
v-for="invitation in currentTeamInvitations"
:key="invitation.uuid"
padding="lg"
class="border-dashed border-warning"
@@ -111,7 +111,15 @@
</template>
<script setup lang="ts">
import { GetTeamDocument } from '~/composables/graphql/user/teams-generated'
import { GetTeamDocument, type GetTeamQueryResult } from '~/composables/graphql/user/teams-generated'
interface UserTeam {
id?: string | null
name: string
logtoOrgId?: string | null
}
type TeamWithMembers = NonNullable<GetTeamQueryResult['getTeam']>
const { t } = useI18n()
const router = useRouter()
@@ -129,8 +137,8 @@ const me = useState<{
} | null>('me', () => null)
const { setActiveTeam } = useActiveTeam()
const userTeams = ref<any[]>([])
const currentTeam = ref<any>(null)
const userTeams = ref<UserTeam[]>([])
const currentTeam = ref<TeamWithMembers | UserTeam | null>(null)
const isLoading = ref(true)
const hasError = ref(false)
const error = ref('')
@@ -143,7 +151,7 @@ const teamHeaderActions = computed(() => {
}
return actions
})
const roleText = (role?: string) => {
const roleText = (role?: string | null) => {
const map: Record<string, string> = {
OWNER: t('clientTeam.roles.owner'),
ADMIN: t('clientTeam.roles.admin'),
@@ -153,13 +161,30 @@ const roleText = (role?: string) => {
return map[role || ''] || role || t('clientTeam.roles.member')
}
const getMemberInitials = (user?: any) => {
interface TeamMember {
id?: string | null
firstName?: string | null
lastName?: string | null
}
const getMemberInitials = (user?: TeamMember | null) => {
if (!user) return '??'
const first = user.firstName?.charAt(0) || ''
const last = user.lastName?.charAt(0) || ''
return (first + last).toUpperCase() || user.id?.charAt(0).toUpperCase() || '??'
}
// Type-safe accessors for TeamWithMembers properties
const currentTeamMembers = computed(() => {
const team = currentTeam.value
return team && 'members' in team ? (team.members || []).filter((m): m is NonNullable<typeof m> => m !== null) : []
})
const currentTeamInvitations = computed(() => {
const team = currentTeam.value
return team && 'invitations' in team ? (team.invitations || []).filter((i): i is NonNullable<typeof i> => i !== null) : []
})
const loadUserTeams = async () => {
try {
isLoading.value = true
@@ -177,13 +202,15 @@ const loadUserTeams = async () => {
currentTeam.value = teamData.value?.getTeam || null
} else if (userTeams.value.length > 0) {
const firstTeam = userTeams.value[0]
setActiveTeam(firstTeam?.id || null, firstTeam?.logtoOrgId)
if (firstTeam) {
setActiveTeam(firstTeam.id || null, firstTeam.logtoOrgId)
currentTeam.value = firstTeam
}
}
// Если нет команды - currentTeam остаётся null, показываем EmptyState
} catch (err: any) {
} catch (err: unknown) {
hasError.value = true
error.value = err.message || t('clientTeam.error.load')
error.value = err instanceof Error ? err.message : t('clientTeam.error.load')
} finally {
isLoading.value = false
}

View File

@@ -95,8 +95,8 @@ const submitInvite = async () => {
} else {
inviteError.value = result?.inviteMember?.message || t('clientTeam.invite.error')
}
} catch (err: any) {
inviteError.value = err.message || t('clientTeam.invite.error')
} catch (err: unknown) {
inviteError.value = err instanceof Error ? err.message : t('clientTeam.invite.error')
} finally {
inviteLoading.value = false
}

View File

@@ -9,8 +9,15 @@ const plugins = [
const pluginConfig = {
scalars: {
DateTime: 'string',
Date: 'string',
Decimal: 'string',
JSONString: 'Record<string, unknown>',
JSON: 'Record<string, unknown>',
UUID: 'string',
BigInt: 'string',
},
useTypeImports: true,
strictScalars: true,
// Add suffix to operation result types to avoid conflicts with schema types
operationResultSuffix: 'Result',
}