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

View File

@@ -22,7 +22,7 @@
:key="option.sourceUuid ?? index" :key="option.sourceUuid ?? index"
:location-name="getOfferData(option.sourceUuid)?.locationName" :location-name="getOfferData(option.sourceUuid)?.locationName"
:product-name="productName" :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" :currency="getOfferData(option.sourceUuid)?.currency"
:unit="getOfferData(option.sourceUuid)?.unit" :unit="getOfferData(option.sourceUuid)?.unit"
:stages="getRouteStages(option)" :stages="getRouteStages(option)"
@@ -81,7 +81,8 @@ interface RoutePathType {
totalTimeSeconds?: number | null totalTimeSeconds?: number | null
stages?: (RouteStage | 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 route = useRoute()
const localePath = useLocalePath() const localePath = useLocalePath()
@@ -90,12 +91,14 @@ const { execute } = useGraphQL()
const productName = computed(() => searchStore.searchForm.product || (route.query.product as string) || 'Товар') const productName = computed(() => searchStore.searchForm.product || (route.query.product as string) || 'Товар')
const locationName = computed(() => searchStore.searchForm.location || (route.query.location 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 // 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) // 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 summaryTitle = computed(() => `${productName.value}${locationName.value}`)
const summaryMeta = computed(() => { const summaryMeta = computed(() => {
@@ -149,14 +152,16 @@ const fetchOffersByHub = async () => {
const offers = offersResponse?.nearestOffers || [] const offers = offersResponse?.nearestOffers || []
// Offers already include routes from backend // Offers already include routes from backend
const offersWithRoutes = offers.map((offer: any) => ({ const offersWithRoutes = offers
sourceUuid: offer.uuid, .filter((offer): offer is NonNullable<OfferWithRouteType> => offer !== null)
sourceName: offer.productName, .map((offer) => ({
sourceLat: offer.latitude, sourceUuid: offer.uuid,
sourceLon: offer.longitude, sourceName: offer.productName,
distanceKm: offer.distanceKm, sourceLat: offer.latitude,
routes: offer.routes || [] sourceLon: offer.longitude,
})) distanceKm: offer.distanceKm,
routes: offer.routes || []
}))
return { offersByHub: offersWithRoutes } return { offersByHub: offersWithRoutes }
} }
@@ -198,10 +203,12 @@ const mapRouteStages = (route: RoutePathType): RouteStageItem[] => {
const getRouteStages = (option: ProductRouteOption) => { const getRouteStages = (option: ProductRouteOption) => {
const route = option.routes?.[0] const route = option.routes?.[0]
if (!route?.stages) return [] if (!route?.stages) return []
return route.stages.filter(Boolean).map((stage: any) => ({ return route.stages
transportType: stage?.transportType, .filter((stage): stage is NonNullable<RouteStageType> => stage !== null)
distanceKm: stage?.distanceKm .map((stage) => ({
})) transportType: stage.transportType,
distanceKm: stage.distanceKm
}))
} }
// Get offer data for card // Get offer data for card
@@ -233,8 +240,8 @@ const loadOfferDetails = async (options: ProductRouteOption[]) => {
return return
} }
const newOffersData = new Map<string, any>() const newOffersData = new Map<string, OfferData>()
const newSuppliersData = new Map<string, any>() const newSuppliersData = new Map<string, SupplierData>()
const teamUuidsToLoad = new Set<string>() const teamUuidsToLoad = new Set<string>()
// First, load all offers // First, load all offers

View File

@@ -35,14 +35,16 @@
</template> </template>
<script setup lang="ts"> <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 searchStore = useSearchStore()
const { data, pending, error, refresh } = await useServerQuery('products', GetProductsDocument, {}, 'public', 'exchange') const { data, pending, error, refresh } = await useServerQuery('products', GetProductsDocument, {}, 'public', 'exchange')
const productsData = computed(() => data.value?.getProducts || []) const productsData = computed(() => data.value?.getProducts || [])
const selectProduct = (product: any) => { const selectProduct = (product: Product) => {
searchStore.setProduct(product.name) searchStore.setProduct(product.name)
searchStore.setProductUuid(product.uuid) searchStore.setProductUuid(product.uuid)
const locationUuid = searchStore.searchForm.locationUuid const locationUuid = searchStore.searchForm.locationUuid

View File

@@ -154,10 +154,44 @@
</template> </template>
<script setup lang="ts"> <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 { t } = useI18n()
const emit = defineEmits<{ const emit = defineEmits<{
submit: [data: any] submit: [data: KycSubmitData]
}>() }>()
const loading = ref(false) const loading = ref(false)
@@ -195,7 +229,7 @@ const isFormValid = computed(() => {
}) })
// Handlers // Handlers
const onCompanySelect = (company: any) => { const onCompanySelect = (company: CompanySuggestion) => {
formData.value.company = { formData.value.company = {
companyName: company.value, companyName: company.value,
companyFullName: company.unrestricted_value, companyFullName: company.unrestricted_value,
@@ -206,7 +240,7 @@ const onCompanySelect = (company: any) => {
} }
} }
const onBankSelect = (bank: any) => { const onBankSelect = (bank: BankSuggestion) => {
formData.value.bank = { formData.value.bank = {
bankName: bank.value, bankName: bank.value,
bik: bank.data.bic, bik: bank.data.bic,

View File

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

View File

@@ -52,8 +52,8 @@
<!-- Hubs Tab --> <!-- Hubs Tab -->
<div v-else-if="activeTab === 'hubs'" class="space-y-2"> <div v-else-if="activeTab === 'hubs'" class="space-y-2">
<HubCard <HubCard
v-for="hub in hubs" v-for="(hub, index) in hubs"
:key="hub.uuid" :key="hub.uuid ?? index"
:hub="hub" :hub="hub"
selectable selectable
:is-selected="selectedItemId === hub.uuid" :is-selected="selectedItemId === hub.uuid"
@@ -67,8 +67,8 @@
<!-- Suppliers Tab --> <!-- Suppliers Tab -->
<div v-else-if="activeTab === 'suppliers'" class="space-y-2"> <div v-else-if="activeTab === 'suppliers'" class="space-y-2">
<SupplierCard <SupplierCard
v-for="supplier in suppliers" v-for="(supplier, index) in suppliers"
:key="supplier.uuid" :key="supplier.uuid ?? index"
:supplier="supplier" :supplier="supplier"
selectable selectable
:is-selected="selectedItemId === supplier.uuid" :is-selected="selectedItemId === supplier.uuid"
@@ -82,8 +82,8 @@
<!-- Offers Tab --> <!-- Offers Tab -->
<div v-else-if="activeTab === 'offers'" class="space-y-2"> <div v-else-if="activeTab === 'offers'" class="space-y-2">
<OfferCard <OfferCard
v-for="offer in offers" v-for="(offer, index) in offers"
:key="offer.uuid" :key="offer.uuid ?? index"
:offer="offer" :offer="offer"
selectable selectable
:is-selected="selectedItemId === offer.uuid" :is-selected="selectedItemId === offer.uuid"
@@ -98,18 +98,56 @@
</template> </template>
<script setup lang="ts"> <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<{ defineProps<{
activeTab: 'hubs' | 'suppliers' | 'offers' activeTab: 'hubs' | 'suppliers' | 'offers'
hubs: any[] hubs: Hub[]
suppliers: any[] suppliers: Supplier[]
offers: any[] offers: Offer[]
selectedItemId: string | null selectedItemId: string | null
isLoading: boolean isLoading: boolean
}>() }>()
defineEmits<{ defineEmits<{
'update:activeTab': [tab: 'hubs' | 'suppliers' | 'offers'] 'update:activeTab': [tab: 'hubs' | 'suppliers' | 'offers']
'select': [item: any, type: string] 'select': [item: Hub | Supplier | Offer, type: 'hub' | 'supplier' | 'offer']
}>() }>()
const localePath = useLocalePath() const localePath = useLocalePath()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,9 +13,9 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean; } Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; } Int: { input: number; output: number; }
Float: { input: number; output: number; } Float: { input: number; output: number; }
Date: { input: any; output: any; } Date: { input: string; output: string; }
DateTime: { input: string; output: string; } DateTime: { input: string; output: string; }
Decimal: { input: any; output: any; } Decimal: { input: string; output: string; }
}; };
export type OfferType = { 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<{ export type GetOfferQueryVariables = Exact<{
uuid: Scalars['String']['input']; 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<{ export type GetOffersQueryVariables = Exact<{
productUuid?: InputMaybe<Scalars['String']['input']>; 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<{ export type GetProductQueryVariables = Exact<{
uuid: Scalars['String']['input']; 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; }>; 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<{ export type GetSupplierProfileQueryVariables = Exact<{
uuid: Scalars['String']['input']; uuid: Scalars['String']['input'];

View File

@@ -13,7 +13,7 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean; } Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; } Int: { input: number; output: number; }
Float: { 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. */ /** 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<{ export type GetClusteredNodesQueryVariables = Exact<{
west: Scalars['Float']['input']; 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<{ export type HubsListQueryVariables = Exact<{
limit?: InputMaybe<Scalars['Int']['input']>; limit?: InputMaybe<Scalars['Int']['input']>;

View File

@@ -13,10 +13,10 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean; } Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; } Int: { input: number; output: number; }
Float: { input: number; output: number; } Float: { input: number; output: number; }
Date: { input: any; output: any; } Date: { input: string; output: string; }
DateTime: { input: string; output: string; } DateTime: { input: string; output: string; }
Decimal: { input: any; output: any; } Decimal: { input: string; output: string; }
JSONString: { input: any; output: any; } JSONString: { input: Record<string, unknown>; output: Record<string, unknown>; }
}; };
export type CreateOffer = { 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<{ export type GetRequestsQueryVariables = Exact<{
userId: Scalars['String']['input']; 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>; 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; } Int: { input: number; output: number; }
Float: { input: number; output: number; } Float: { input: number; output: number; }
DateTime: { input: string; output: string; } 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. */ /** 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<{ export type GetKycRequestRussiaQueryVariables = Exact<{
uuid: Scalars['String']['input']; 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 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>; 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 itemsByCountry = computed(() => {
const grouped = new Map<string, any[]>() const grouped = new Map<string, Array<HubItem | NearestHubItem>>()
items.value.forEach(hub => { items.value.forEach(hub => {
const country = hub.country || t('catalogMap.labels.country_unknown') const country = hub.country || t('catalogMap.labels.country_unknown')
if (!grouped.has(country)) grouped.set(country, []) 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 { import {
ProductsListDocument, ProductsListDocument,
GetNodeDocument, GetNodeDocument,
@@ -10,6 +10,14 @@ import {
// Type from codegen // Type from codegen
type ProductItem = NonNullable<NonNullable<ProductsListQueryResult['productsList']>[number]> 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 // Shared state
const items = ref<ProductItem[]>([]) const items = ref<ProductItem[]>([])
@@ -61,20 +69,19 @@ export function useCatalogProducts() {
) )
// Group offers by product // Group offers by product
const productsMap = new Map<string, any>() const productsMap = new Map<string, AggregatedProduct>()
offersData?.nearestOffers?.forEach((offer: any) => { offersData?.nearestOffers?.forEach((offer) => {
if (offer?.productUuid) { if (!offer?.productUuid) return
if (!productsMap.has(offer.productUuid)) { if (!productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, { productsMap.set(offer.productUuid, {
uuid: offer.productUuid, uuid: offer.productUuid,
name: offer.productName, name: offer.productName,
offersCount: 0 offersCount: 0
}) })
}
productsMap.get(offer.productUuid)!.offersCount++
} }
productsMap.get(offer.productUuid)!.offersCount++
}) })
items.value = Array.from(productsMap.values()) items.value = Array.from(productsMap.values()) as ProductItem[]
} }
} else if (filterHubUuid.value) { } else if (filterHubUuid.value) {
// Products near hub - get hub coordinates first // Products near hub - get hub coordinates first
@@ -103,20 +110,19 @@ export function useCatalogProducts() {
) )
// Group offers by product // Group offers by product
const productsMap = new Map<string, any>() const productsMap = new Map<string, AggregatedProduct>()
offersData?.nearestOffers?.forEach((offer: any) => { offersData?.nearestOffers?.forEach((offer) => {
if (offer?.productUuid) { if (!offer?.productUuid) return
if (!productsMap.has(offer.productUuid)) { if (!productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, { productsMap.set(offer.productUuid, {
uuid: offer.productUuid, uuid: offer.productUuid,
name: offer.productName, name: offer.productName,
offersCount: 0 offersCount: 0
}) })
}
productsMap.get(offer.productUuid)!.offersCount++
} }
productsMap.get(offer.productUuid)!.offersCount++
}) })
items.value = Array.from(productsMap.values()) items.value = Array.from(productsMap.values()) as ProductItem[]
} }
} else { } else {
// All products from graph // 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 isLoading = ref(false)
const isInitialized = ref(false) const isInitialized = ref(false)
@@ -23,7 +27,7 @@ export function useTeamAddresses() {
try { try {
const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated') const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated')
const data = await execute(GetTeamAddressesDocument, {}, 'team', 'teams') 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 isInitialized.value = true
} catch (e) { } catch (e) {
console.error('Failed to load addresses', 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 isLoading = ref(false)
const isInitialized = ref(false) const isInitialized = ref(false)
@@ -23,13 +28,14 @@ export function useTeamOrders() {
const routesForMap = computed(() => const routesForMap = computed(() =>
filteredItems.value filteredItems.value
.filter(order => order.uuid && order.name)
.map(order => ({ .map(order => ({
uuid: order.uuid, uuid: order.uuid!,
name: order.name, name: order.name!,
status: order.status, status: order.status ?? undefined,
stages: (order.stages || []) stages: (order.stages || [])
.filter((s: any) => s.stageType === 'transport' && s.sourceLatitude && s.sourceLongitude && s.destinationLatitude && s.destinationLongitude) .filter((s): s is TeamOrderStage => s !== null && s.stageType === 'transport' && !!s.sourceLatitude && !!s.sourceLongitude && !!s.destinationLatitude && !!s.destinationLongitude)
.map((s: any) => ({ .map((s) => ({
fromLat: s.sourceLatitude, fromLat: s.sourceLatitude,
fromLon: s.sourceLongitude, fromLon: s.sourceLongitude,
toLat: s.destinationLatitude, toLat: s.destinationLatitude,
@@ -47,7 +53,7 @@ export function useTeamOrders() {
try { try {
const { GetTeamOrdersDocument } = await import('~/composables/graphql/team/orders-generated') const { GetTeamOrdersDocument } = await import('~/composables/graphql/team/orders-generated')
const data = await execute(GetTeamOrdersDocument, {}, 'team', 'orders') 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 isInitialized.value = true
} catch (e) { } catch (e) {
console.error('Failed to load orders', e) console.error('Failed to load orders', e)

View File

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

View File

@@ -44,7 +44,9 @@
</template> </template>
<script setup lang="ts"> <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({ definePageMeta({
layout: 'topnav' layout: 'topnav'
@@ -56,7 +58,7 @@ const { t } = useI18n()
const isLoading = ref(true) const isLoading = ref(true)
const hoveredId = ref<string>() const hoveredId = ref<string>()
const hub = ref<any>(null) const hub = ref<Hub | null>(null)
const products = ref<Array<{ uuid: string; name: string }>>([]) const products = ref<Array<{ uuid: string; name: string }>>([])
const hubId = computed(() => route.params.id as string) const hubId = computed(() => route.params.id as string)
@@ -150,7 +152,7 @@ try {
// Group offers by product // Group offers by product
const productsMap = new Map<string, { uuid: string; name: string }>() const productsMap = new Map<string, { uuid: string; name: string }>()
offersData.value?.nearestOffers?.forEach((offer: any) => { offersData.value?.nearestOffers?.forEach((offer) => {
if (offer?.productUuid) { if (offer?.productUuid) {
if (!productsMap.has(offer.productUuid)) { if (!productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, { productsMap.set(offer.productUuid, {

View File

@@ -37,19 +37,19 @@
</div> </div>
<!-- Location on map --> <!-- 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> <ClientOnly>
<MapboxMap <MapboxMap
map-id="offer-location-map" map-id="offer-location-map"
class="w-full h-full" class="w-full h-full"
:options="{ :options="{
style: 'mapbox://styles/mapbox/streets-v12', style: 'mapbox://styles/mapbox/streets-v12',
center: [offer.longitude, offer.latitude], center: [offer.locationLongitude, offer.locationLatitude],
zoom: 8 zoom: 8
}" }"
> >
<MapboxDefaultMarker <MapboxDefaultMarker
:lnglat="[offer.longitude, offer.latitude]" :lnglat="[offer.locationLongitude, offer.locationLatitude]"
color="#10b981" color="#10b981"
/> />
</MapboxMap> </MapboxMap>
@@ -101,7 +101,10 @@
</template> </template>
<script setup lang="ts"> <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({ definePageMeta({
layout: 'topnav' layout: 'topnav'
@@ -114,8 +117,8 @@ const { execute } = useGraphQL()
const offerId = computed(() => route.params.offerId as string) const offerId = computed(() => route.params.offerId as string)
const isLoading = ref(true) const isLoading = ref(true)
const offer = ref<any>(null) const offer = ref<Offer | null>(null)
const supplier = ref<any>(null) const supplier = ref<SupplierProfile | null>(null)
// Load offer data // Load offer data
const loadOffer = async () => { const loadOffer = async () => {

View File

@@ -106,7 +106,7 @@
</Stack> </Stack>
<!-- Line with this product --> <!-- 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"> <Card padding="sm" class="bg-base-200">
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Stack gap="0"> <Stack gap="0">
@@ -204,8 +204,14 @@ import {
GetProductsDocument, GetProductsDocument,
GetProductOffersDocument, GetProductOffersDocument,
GetSupplierProfilesDocument, GetSupplierProfilesDocument,
type GetProductsQueryResult,
type GetProductOffersQueryResult
} from '~/composables/graphql/public/exchange-generated' } from '~/composables/graphql/public/exchange-generated'
// Types from GraphQL
type Product = NonNullable<NonNullable<GetProductsQueryResult['getProducts']>[number]>
type ProductOffer = NonNullable<NonNullable<GetProductOffersQueryResult['getOffers']>[number]>
definePageMeta({ definePageMeta({
layout: 'topnav' layout: 'topnav'
}) })
@@ -237,7 +243,7 @@ const allSuppliers = computed(() => suppliersData.value?.getSupplierProfiles ||
const productId = computed(() => route.params.id as string) const productId = computed(() => route.params.id as string)
// Find product by uuid from list // Find product by uuid from list
const findProduct = (products: any[]) => { const findProduct = (products: (Product | null)[]) => {
return products.find(p => p?.uuid === productId.value) return products.find(p => p?.uuid === productId.value)
} }
@@ -295,11 +301,13 @@ const mapLocations = computed(() => {
const priceRange = computed(() => { const priceRange = computed(() => {
const prices: number[] = [] const prices: number[] = []
offers.value.forEach(offer => { offers.value.forEach(offer => {
(offer as any).lines?.forEach((line: any) => { // Offers for this product already filtered by productUuid
if (line?.productUuid === productId.value && line?.pricePerUnit) { if (offer.pricePerUnit) {
prices.push(Number(line.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') if (prices.length === 0) return t('common.values.not_available')
const min = Math.min(...prices) const min = Math.min(...prices)
@@ -308,9 +316,24 @@ const priceRange = computed(() => {
return t('catalogProduct.labels.price_range', { min: min.toLocaleString(), max: max.toLocaleString() }) return t('catalogProduct.labels.price_range', { min: min.toLocaleString(), max: max.toLocaleString() })
}) })
// Get lines with this product // Get offer as "line" - offers already have quantity/unit/price directly
const getProductLines = (offer: any) => { interface OfferLine {
return (offer.lines || []).filter((line: any) => line?.productUuid === productId.value) 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) => { 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 '—' if (!price) return '—'
const num = Number(price) const num = typeof price === 'string' ? parseFloat(price) : Number(price)
if (isNaN(num)) return '—'
const curr = currency || 'USD' const curr = currency || 'USD'
try { try {
return new Intl.NumberFormat('ru', { return new Intl.NumberFormat('ru', {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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