refactor: remove all any types, add strict GraphQL scalar typing
All checks were successful
Build Docker Image / build (push) Successful in 4m3s
All checks were successful
Build Docker Image / build (push) Successful in 4m3s
- Add strictScalars: true to codegen.ts with proper scalar mappings (Date, Decimal, JSONString, JSON, UUID, BigInt → string/Record) - Replace all ref<any[]> with proper GraphQL-derived types - Add type guards for null filtering in arrays - Fix bugs exposed by typing (locationLatitude vs latitude, etc.) - Add interfaces for external components (MapboxSearchBox) This enables end-to-end type safety from GraphQL schema to frontend.
This commit is contained in:
@@ -44,7 +44,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetNodeDocument, NearestOffersDocument } from '~/composables/graphql/public/geo-generated'
|
||||
import { GetNodeDocument, NearestOffersDocument, type OfferWithRouteType, type GetNodeQueryResult } from '~/composables/graphql/public/geo-generated'
|
||||
|
||||
type Hub = NonNullable<GetNodeQueryResult['node']>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav'
|
||||
@@ -56,7 +58,7 @@ const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(true)
|
||||
const hoveredId = ref<string>()
|
||||
const hub = ref<any>(null)
|
||||
const hub = ref<Hub | null>(null)
|
||||
const products = ref<Array<{ uuid: string; name: string }>>([])
|
||||
|
||||
const hubId = computed(() => route.params.id as string)
|
||||
@@ -150,7 +152,7 @@ try {
|
||||
|
||||
// Group offers by product
|
||||
const productsMap = new Map<string, { uuid: string; name: string }>()
|
||||
offersData.value?.nearestOffers?.forEach((offer: any) => {
|
||||
offersData.value?.nearestOffers?.forEach((offer) => {
|
||||
if (offer?.productUuid) {
|
||||
if (!productsMap.has(offer.productUuid)) {
|
||||
productsMap.set(offer.productUuid, {
|
||||
|
||||
@@ -37,19 +37,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Location on map -->
|
||||
<div v-if="offer.latitude && offer.longitude" class="h-48 rounded-lg overflow-hidden">
|
||||
<div v-if="offer.locationLatitude && offer.locationLongitude" class="h-48 rounded-lg overflow-hidden">
|
||||
<ClientOnly>
|
||||
<MapboxMap
|
||||
map-id="offer-location-map"
|
||||
class="w-full h-full"
|
||||
:options="{
|
||||
style: 'mapbox://styles/mapbox/streets-v12',
|
||||
center: [offer.longitude, offer.latitude],
|
||||
center: [offer.locationLongitude, offer.locationLatitude],
|
||||
zoom: 8
|
||||
}"
|
||||
>
|
||||
<MapboxDefaultMarker
|
||||
:lnglat="[offer.longitude, offer.latitude]"
|
||||
:lnglat="[offer.locationLongitude, offer.locationLatitude]"
|
||||
color="#10b981"
|
||||
/>
|
||||
</MapboxMap>
|
||||
@@ -101,7 +101,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetOfferDocument, GetSupplierProfileByTeamDocument } from '~/composables/graphql/public/exchange-generated'
|
||||
import { GetOfferDocument, GetSupplierProfileByTeamDocument, type GetOfferQueryResult, type GetSupplierProfileByTeamQueryResult } from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
type Offer = NonNullable<GetOfferQueryResult['getOffer']>
|
||||
type SupplierProfile = NonNullable<GetSupplierProfileByTeamQueryResult['getSupplierProfileByTeam']>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav'
|
||||
@@ -114,8 +117,8 @@ const { execute } = useGraphQL()
|
||||
const offerId = computed(() => route.params.offerId as string)
|
||||
|
||||
const isLoading = ref(true)
|
||||
const offer = ref<any>(null)
|
||||
const supplier = ref<any>(null)
|
||||
const offer = ref<Offer | null>(null)
|
||||
const supplier = ref<SupplierProfile | null>(null)
|
||||
|
||||
// Load offer data
|
||||
const loadOffer = async () => {
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</Stack>
|
||||
|
||||
<!-- Line with this product -->
|
||||
<template v-for="line in getProductLines(offer)" :key="line?.uuid">
|
||||
<template v-for="(line, lineIndex) in getProductLines(offer)" :key="line?.uuid ?? lineIndex">
|
||||
<Card padding="sm" class="bg-base-200">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack gap="0">
|
||||
@@ -204,8 +204,14 @@ import {
|
||||
GetProductsDocument,
|
||||
GetProductOffersDocument,
|
||||
GetSupplierProfilesDocument,
|
||||
type GetProductsQueryResult,
|
||||
type GetProductOffersQueryResult
|
||||
} from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
// Types from GraphQL
|
||||
type Product = NonNullable<NonNullable<GetProductsQueryResult['getProducts']>[number]>
|
||||
type ProductOffer = NonNullable<NonNullable<GetProductOffersQueryResult['getOffers']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav'
|
||||
})
|
||||
@@ -237,7 +243,7 @@ const allSuppliers = computed(() => suppliersData.value?.getSupplierProfiles ||
|
||||
const productId = computed(() => route.params.id as string)
|
||||
|
||||
// Find product by uuid from list
|
||||
const findProduct = (products: any[]) => {
|
||||
const findProduct = (products: (Product | null)[]) => {
|
||||
return products.find(p => p?.uuid === productId.value)
|
||||
}
|
||||
|
||||
@@ -295,11 +301,13 @@ const mapLocations = computed(() => {
|
||||
const priceRange = computed(() => {
|
||||
const prices: number[] = []
|
||||
offers.value.forEach(offer => {
|
||||
(offer as any).lines?.forEach((line: any) => {
|
||||
if (line?.productUuid === productId.value && line?.pricePerUnit) {
|
||||
prices.push(Number(line.pricePerUnit))
|
||||
// Offers for this product already filtered by productUuid
|
||||
if (offer.pricePerUnit) {
|
||||
const price = typeof offer.pricePerUnit === 'string' ? parseFloat(offer.pricePerUnit) : Number(offer.pricePerUnit)
|
||||
if (!isNaN(price)) {
|
||||
prices.push(price)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if (prices.length === 0) return t('common.values.not_available')
|
||||
const min = Math.min(...prices)
|
||||
@@ -308,9 +316,24 @@ const priceRange = computed(() => {
|
||||
return t('catalogProduct.labels.price_range', { min: min.toLocaleString(), max: max.toLocaleString() })
|
||||
})
|
||||
|
||||
// Get lines with this product
|
||||
const getProductLines = (offer: any) => {
|
||||
return (offer.lines || []).filter((line: any) => line?.productUuid === productId.value)
|
||||
// Get offer as "line" - offers already have quantity/unit/price directly
|
||||
interface OfferLine {
|
||||
uuid?: string | null
|
||||
quantity?: string | number | null
|
||||
unit?: string | null
|
||||
pricePerUnit?: string | number | null
|
||||
currency?: string | null
|
||||
}
|
||||
|
||||
const getProductLines = (offer: ProductOffer): OfferLine[] => {
|
||||
// Each offer is a single "line" with quantity, unit, and price
|
||||
return [{
|
||||
uuid: offer.uuid,
|
||||
quantity: offer.quantity,
|
||||
unit: offer.unit,
|
||||
pricePerUnit: offer.pricePerUnit,
|
||||
currency: offer.currency
|
||||
}]
|
||||
}
|
||||
|
||||
const getCategoryIcon = (categoryName: string | null | undefined) => {
|
||||
@@ -355,9 +378,10 @@ const formatDate = (dateStr: string | null | undefined) => {
|
||||
}
|
||||
}
|
||||
|
||||
const formatPrice = (price: any, currency: string | null | undefined) => {
|
||||
const formatPrice = (price: string | number | null | undefined, currency: string | null | undefined) => {
|
||||
if (!price) return '—'
|
||||
const num = Number(price)
|
||||
const num = typeof price === 'string' ? parseFloat(price) : Number(price)
|
||||
if (isNaN(num)) return '—'
|
||||
const curr = currency || 'USD'
|
||||
try {
|
||||
return new Intl.NumberFormat('ru', {
|
||||
|
||||
@@ -94,6 +94,10 @@
|
||||
import { NuxtLink } from '#components'
|
||||
import type { MapMouseEvent, Map as MapboxMapType } from 'mapbox-gl'
|
||||
|
||||
interface MapboxSearchBox {
|
||||
value: string
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
middleware: ['auth-oidc']
|
||||
@@ -112,7 +116,7 @@ const isSaving = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
const searchBoxContainer = ref<HTMLElement | null>(null)
|
||||
const mapInstance = ref<MapboxMapType | null>(null)
|
||||
const searchBoxRef = ref<any>(null)
|
||||
const searchBoxRef = ref<MapboxSearchBox | null>(null)
|
||||
|
||||
const addressData = ref<{
|
||||
uuid: string
|
||||
@@ -130,7 +134,7 @@ const loadAddress = async () => {
|
||||
const { GetTeamAddressesDocument } = await import('~/composables/graphql/team/teams-generated')
|
||||
const data = await execute(GetTeamAddressesDocument, {}, 'team', 'teams')
|
||||
const addresses = data?.teamAddresses || []
|
||||
const found = addresses.find((a: any) => a.uuid === uuid.value)
|
||||
const found = addresses.find((a) => a?.uuid === uuid.value)
|
||||
|
||||
if (found) {
|
||||
addressData.value = {
|
||||
@@ -167,7 +171,7 @@ const reverseGeocode = async (lat: number, lng: number): Promise<{ address: stri
|
||||
if (!feature) return { address: null, countryCode: null }
|
||||
|
||||
// Extract country code from context
|
||||
const countryContext = feature.context?.find((c: any) => c.id?.startsWith('country.'))
|
||||
const countryContext = feature.context?.find((c: { id?: string }) => c.id?.startsWith('country.'))
|
||||
const countryCode = countryContext?.short_code?.toUpperCase() || null
|
||||
|
||||
return { address: feature.place_name, countryCode }
|
||||
@@ -215,7 +219,7 @@ onMounted(async () => {
|
||||
searchBox.value = addressData.value.address
|
||||
}
|
||||
|
||||
searchBox.addEventListener('retrieve', (event: any) => {
|
||||
searchBox.addEventListener('retrieve', (event: CustomEvent) => {
|
||||
if (!addressData.value) return
|
||||
|
||||
const feature = event.detail.features?.[0]
|
||||
|
||||
@@ -118,8 +118,10 @@ const onSearch = () => {
|
||||
// TODO: Implement search
|
||||
}
|
||||
|
||||
const onSelectAddress = (item: any) => {
|
||||
selectedAddressId.value = item.uuid
|
||||
const onSelectAddress = (item: { uuid?: string | null }) => {
|
||||
if (item.uuid) {
|
||||
selectedAddressId.value = item.uuid
|
||||
}
|
||||
}
|
||||
|
||||
await init()
|
||||
|
||||
@@ -62,10 +62,12 @@ await init()
|
||||
const mapRef = ref<{ flyTo: (lat: number, lng: number, zoom?: number) => void } | null>(null)
|
||||
const selectedItemId = ref<string | null>(null)
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
selectedItemId.value = item.uuid
|
||||
if (item.latitude && item.longitude) {
|
||||
mapRef.value?.flyTo(item.latitude, item.longitude, 8)
|
||||
const selectItem = (item: { uuid?: string | null; latitude?: number | null; longitude?: number | null }) => {
|
||||
if (item.uuid) {
|
||||
selectedItemId.value = item.uuid
|
||||
if (item.latitude && item.longitude) {
|
||||
mapRef.value?.flyTo(item.latitude, item.longitude, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,10 @@
|
||||
import { NuxtLink } from '#components'
|
||||
import type { MapMouseEvent, Map as MapboxMapType } from 'mapbox-gl'
|
||||
|
||||
interface MapboxSearchBox {
|
||||
value: string
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
middleware: ['auth-oidc']
|
||||
@@ -84,7 +88,7 @@ const config = useRuntimeConfig()
|
||||
const isCreating = ref(false)
|
||||
const searchBoxContainer = ref<HTMLElement | null>(null)
|
||||
const mapInstance = ref<MapboxMapType | null>(null)
|
||||
const searchBoxRef = ref<any>(null)
|
||||
const searchBoxRef = ref<MapboxSearchBox | null>(null)
|
||||
|
||||
const newAddress = reactive({
|
||||
name: '',
|
||||
@@ -110,7 +114,7 @@ const reverseGeocode = async (lat: number, lng: number): Promise<{ address: stri
|
||||
if (!feature) return { address: null, countryCode: null }
|
||||
|
||||
// Extract country code from context
|
||||
const countryContext = feature.context?.find((c: any) => c.id?.startsWith('country.'))
|
||||
const countryContext = feature.context?.find((c: { id?: string }) => c.id?.startsWith('country.'))
|
||||
const countryCode = countryContext?.short_code?.toUpperCase() || null
|
||||
|
||||
return { address: feature.place_name, countryCode }
|
||||
@@ -151,7 +155,7 @@ onMounted(async () => {
|
||||
}
|
||||
searchBox.placeholder = t('profileAddresses.form.address.placeholder')
|
||||
|
||||
searchBox.addEventListener('retrieve', (event: any) => {
|
||||
searchBox.addEventListener('retrieve', (event: CustomEvent) => {
|
||||
const feature = event.detail.features?.[0]
|
||||
if (feature) {
|
||||
const [lng, lat] = feature.geometry.coordinates
|
||||
|
||||
@@ -109,9 +109,9 @@ const handleSend = async () => {
|
||||
const content = last?.content?.[0]?.text || last?.content || t('aiAssistants.view.emptyResponse')
|
||||
chat.value.push({ role: 'assistant', content })
|
||||
scrollToBottom()
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
console.error('Agent error', e)
|
||||
error.value = e?.message || t('aiAssistants.view.error')
|
||||
error.value = e instanceof Error ? e.message : t('aiAssistants.view.error')
|
||||
chat.value.push({ role: 'assistant', content: t('aiAssistants.view.error') })
|
||||
scrollToBottom()
|
||||
} finally {
|
||||
|
||||
@@ -95,6 +95,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GetTeamTransactionsQueryResult } from '~/composables/graphql/team/billing-generated'
|
||||
|
||||
type Transaction = NonNullable<NonNullable<GetTeamTransactionsQueryResult['teamTransactions']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
middleware: ['auth-oidc']
|
||||
@@ -112,7 +116,7 @@ const balance = ref({
|
||||
exists: false
|
||||
})
|
||||
|
||||
const transactions = ref<any[]>([])
|
||||
const transactions = ref<Transaction[]>([])
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
// Amount is in kopecks, convert to base units
|
||||
@@ -130,7 +134,7 @@ const formatAmount = (amount: number) => {
|
||||
}).format(amount / 100)
|
||||
}
|
||||
|
||||
const formatTimestamp = (timestamp: number) => {
|
||||
const formatTimestamp = (timestamp: number | null | undefined) => {
|
||||
if (!timestamp) return '—'
|
||||
// TigerBeetle timestamp is in nanoseconds since epoch
|
||||
const date = new Date(timestamp / 1000000)
|
||||
@@ -157,8 +161,8 @@ const loadBalance = async () => {
|
||||
if (data.value?.teamBalance) {
|
||||
balance.value = data.value.teamBalance
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || t('billing.errors.load_failed')
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : t('billing.errors.load_failed')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
@@ -171,7 +175,7 @@ const loadTransactions = async () => {
|
||||
|
||||
if (txError.value) throw txError.value
|
||||
|
||||
transactions.value = data.value?.teamTransactions || []
|
||||
transactions.value = (data.value?.teamTransactions || []).filter((tx): tx is Transaction => tx !== null)
|
||||
} catch (e) {
|
||||
console.error('Failed to load transactions', e)
|
||||
}
|
||||
|
||||
@@ -146,8 +146,8 @@ const switchToTeam = async (teamId: string) => {
|
||||
markActiveTeam(newActiveId)
|
||||
navigateTo(localePath('/clientarea/team'))
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || t('clientTeamSwitch.error.switch')
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : t('clientTeamSwitch.error.switch')
|
||||
hasError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<Card v-for="request in kycRequests" :key="request.uuid" padding="lg">
|
||||
<Stack gap="3">
|
||||
<Stack direction="row" gap="2" align="center" justify="between">
|
||||
<Heading :level="4" weight="semibold">{{ request.companyName || t('kycOverview.list.unnamed') }}</Heading>
|
||||
<Heading :level="4" weight="semibold">{{ request.teamName || t('kycOverview.list.unnamed') }}</Heading>
|
||||
<Pill :variant="getStatusVariant(request)" :tone="getStatusTone(request)">
|
||||
{{ getStatusText(request) }}
|
||||
</Pill>
|
||||
@@ -32,8 +32,8 @@
|
||||
<Text tone="muted" size="base">
|
||||
{{ t('kycOverview.list.submitted') }}: {{ formatDate(request.createdAt) }}
|
||||
</Text>
|
||||
<Text v-if="request.inn" tone="muted" size="base">
|
||||
{{ t('kycOverview.list.inn') }}: {{ request.inn }}
|
||||
<Text tone="muted" size="base">
|
||||
{{ t('kycOverview.list.country') }}: {{ request.countryCode }}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -91,7 +91,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetKycRequestsRussiaDocument } from '~/composables/graphql/user/kyc-generated'
|
||||
import { GetKycRequestsRussiaDocument, type GetKycRequestsRussiaQueryResult } from '~/composables/graphql/user/kyc-generated'
|
||||
|
||||
type KycRequest = NonNullable<NonNullable<GetKycRequestsRussiaQueryResult['kycRequests']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
@@ -102,7 +104,7 @@ const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const kycRequests = ref<any[]>([])
|
||||
const kycRequests = ref<KycRequest[]>([])
|
||||
|
||||
const selectCountry = (country: string) => {
|
||||
if (country === 'russia') {
|
||||
@@ -110,21 +112,18 @@ const selectCountry = (country: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusVariant = (request: any) => {
|
||||
const getStatusVariant = (request: KycRequest) => {
|
||||
if (request.approvedAt) return 'primary'
|
||||
if (request.rejectedAt) return 'outline'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
const getStatusTone = (request: any) => {
|
||||
const getStatusTone = (request: KycRequest) => {
|
||||
if (request.approvedAt) return 'success'
|
||||
if (request.rejectedAt) return 'error'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
const getStatusText = (request: any) => {
|
||||
const getStatusText = (request: KycRequest) => {
|
||||
if (request.approvedAt) return t('kycOverview.list.status.approved')
|
||||
if (request.rejectedAt) return t('kycOverview.list.status.rejected')
|
||||
return t('kycOverview.list.status.pending')
|
||||
}
|
||||
|
||||
@@ -143,10 +142,10 @@ const loadKYCStatus = async () => {
|
||||
if (kycError.value) throw kycError.value
|
||||
const requests = data.value?.kycRequests || []
|
||||
// Сортируем по дате создания (новые первые)
|
||||
kycRequests.value = [...requests].sort((a: any, b: any) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
} catch (err: any) {
|
||||
kycRequests.value = [...requests]
|
||||
.filter((r): r is KycRequest => r !== null)
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
} catch (err: unknown) {
|
||||
error.value = t('kycOverview.errors.load_failed')
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -57,24 +57,39 @@ const submitting = ref(false)
|
||||
const submitError = ref<string | null>(null)
|
||||
const submitSuccess = ref(false)
|
||||
|
||||
const handleSubmit = async (formData: any) => {
|
||||
interface KycFormData {
|
||||
company_name?: string
|
||||
company_full_name?: string
|
||||
inn?: string
|
||||
kpp?: string
|
||||
ogrn?: string
|
||||
address?: string
|
||||
bank_name?: string
|
||||
bik?: string
|
||||
correspondent_account?: string
|
||||
contact_person?: string
|
||||
contact_email?: string
|
||||
contact_phone?: string
|
||||
}
|
||||
|
||||
const handleSubmit = async (formData: KycFormData) => {
|
||||
try {
|
||||
submitting.value = true
|
||||
submitError.value = null
|
||||
|
||||
const submitData = {
|
||||
companyName: formData.company_name,
|
||||
companyFullName: formData.company_full_name,
|
||||
inn: formData.inn,
|
||||
companyName: formData.company_name || '',
|
||||
companyFullName: formData.company_full_name || '',
|
||||
inn: formData.inn || '',
|
||||
kpp: formData.kpp || '',
|
||||
ogrn: formData.ogrn || '',
|
||||
address: formData.address,
|
||||
bankName: formData.bank_name,
|
||||
bik: formData.bik,
|
||||
address: formData.address || '',
|
||||
bankName: formData.bank_name || '',
|
||||
bik: formData.bik || '',
|
||||
correspondentAccount: formData.correspondent_account || '',
|
||||
contactPerson: formData.contact_person,
|
||||
contactEmail: formData.contact_email,
|
||||
contactPhone: formData.contact_phone,
|
||||
contactPerson: formData.contact_person || '',
|
||||
contactEmail: formData.contact_email || '',
|
||||
contactPhone: formData.contact_phone || '',
|
||||
}
|
||||
|
||||
const result = await mutate(CreateKycApplicationRussiaDocument, { input: submitData }, 'user', 'kyc')
|
||||
@@ -85,8 +100,8 @@ const handleSubmit = async (formData: any) => {
|
||||
} else {
|
||||
throw new Error(t('kycRussia.errors.create_failed'))
|
||||
}
|
||||
} catch (err: any) {
|
||||
submitError.value = err.message || t('kycRussia.errors.submit_failed')
|
||||
} catch (err: unknown) {
|
||||
submitError.value = err instanceof Error ? err.message : t('kycRussia.errors.submit_failed')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -118,7 +118,9 @@ import { FormKitSchema } from '@formkit/vue'
|
||||
import type { FormKitSchemaNode } from '@formkit/core'
|
||||
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
|
||||
import { CreateOfferDocument } from '~/composables/graphql/team/exchange-generated'
|
||||
import { GetTeamAddressesDocument } from '~/composables/graphql/team/teams-generated'
|
||||
import { GetTeamAddressesDocument, type GetTeamAddressesQueryResult } from '~/composables/graphql/team/teams-generated'
|
||||
|
||||
type TeamAddress = NonNullable<NonNullable<GetTeamAddressesQueryResult['teamAddresses']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
@@ -147,7 +149,7 @@ const productName = ref<string>('')
|
||||
const schemaId = ref<string | null>(null)
|
||||
const schemaDescription = ref<string | null>(null)
|
||||
const formkitSchema = ref<FormKitSchemaNode[]>([])
|
||||
const addresses = ref<any[]>([])
|
||||
const addresses = ref<TeamAddress[]>([])
|
||||
const selectedAddressUuid = ref<string | null>(null)
|
||||
const formKitConfig = {
|
||||
classes: {
|
||||
@@ -169,8 +171,8 @@ const loadAddresses = async () => {
|
||||
try {
|
||||
const { data, error: addressesError } = await useServerQuery('offer-form-addresses', GetTeamAddressesDocument, {}, 'team', 'teams')
|
||||
if (addressesError.value) throw addressesError.value
|
||||
addresses.value = data.value?.teamAddresses || []
|
||||
const defaultAddress = addresses.value.find((address: any) => address.isDefault)
|
||||
addresses.value = (data.value?.teamAddresses || []).filter((a): a is TeamAddress => a !== null)
|
||||
const defaultAddress = addresses.value.find((address) => address.isDefault)
|
||||
selectedAddressUuid.value = defaultAddress?.uuid || addresses.value[0]?.uuid || null
|
||||
} catch (err) {
|
||||
console.error('Failed to load addresses:', err)
|
||||
@@ -189,7 +191,7 @@ const loadData = async () => {
|
||||
const { data: productsData, error: productsError } = await useServerQuery('offer-form-products', GetProductsDocument, {}, 'public', 'exchange')
|
||||
if (productsError.value) throw productsError.value
|
||||
const products = productsData.value?.getProducts || []
|
||||
const product = products.find((p: any) => p.uuid === productUuid.value)
|
||||
const product = products.find((p) => p?.uuid === productUuid.value)
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t('clientOfferForm.errors.productNotFound', { uuid: productUuid.value }))
|
||||
@@ -219,9 +221,9 @@ const loadData = async () => {
|
||||
formkitSchema.value = schemaToFormKit(terminusClass, enums)
|
||||
await loadAddresses()
|
||||
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
hasError.value = true
|
||||
error.value = err.message || t('clientOfferForm.error.load')
|
||||
error.value = err instanceof Error ? err.message : t('clientOfferForm.error.load')
|
||||
console.error('Load error:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@@ -237,7 +239,7 @@ const handleSubmit = async (data: Record<string, unknown>) => {
|
||||
throw new Error(t('clientOfferForm.error.load'))
|
||||
}
|
||||
|
||||
const selectedAddress = addresses.value.find((address: any) => address.uuid === selectedAddressUuid.value)
|
||||
const selectedAddress = addresses.value.find((address) => address?.uuid === selectedAddressUuid.value)
|
||||
if (!selectedAddress) {
|
||||
throw new Error(t('clientOfferForm.error.save'))
|
||||
}
|
||||
@@ -253,14 +255,14 @@ const handleSubmit = async (data: Record<string, unknown>) => {
|
||||
locationCountryCode: selectedAddress.countryCode || '',
|
||||
locationLatitude: selectedAddress.latitude,
|
||||
locationLongitude: selectedAddress.longitude,
|
||||
quantity: data.quantity || 0,
|
||||
quantity: String(data.quantity || '0'),
|
||||
unit: String(data.unit || 'ton'),
|
||||
pricePerUnit: data.price_per_unit || data.pricePerUnit || null,
|
||||
pricePerUnit: String(data.price_per_unit || data.pricePerUnit || ''),
|
||||
currency: String(data.currency || 'USD'),
|
||||
description: String(data.description || ''),
|
||||
validUntil: data.valid_until || data.validUntil || null,
|
||||
validUntil: (data.valid_until as string | undefined) ?? (data.validUntil as string | undefined) ?? undefined,
|
||||
terminusSchemaId: schemaId.value,
|
||||
terminusPayload: JSON.stringify(data),
|
||||
terminusPayload: data,
|
||||
}
|
||||
|
||||
const result = await mutate(CreateOfferDocument, { input }, 'team', 'exchange')
|
||||
@@ -270,8 +272,8 @@ const handleSubmit = async (data: Record<string, unknown>) => {
|
||||
|
||||
await navigateTo(localePath('/clientarea/offers'))
|
||||
|
||||
} catch (err: any) {
|
||||
error.value = err.message || t('clientOfferForm.error.save')
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : t('clientOfferForm.error.save')
|
||||
hasError.value = true
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
|
||||
@@ -122,7 +122,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
|
||||
import { GetOffersDocument } from '~/composables/graphql/public/exchange-generated'
|
||||
import { GetOffersDocument, type GetOffersQueryResult } from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
type Offer = NonNullable<NonNullable<GetOffersQueryResult['getOffers']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
@@ -135,7 +137,7 @@ const { activeTeamId } = useActiveTeam()
|
||||
|
||||
const { execute } = useGraphQL()
|
||||
const PAGE_SIZE = 24
|
||||
const offers = ref<any[]>([])
|
||||
const offers = ref<Offer[]>([])
|
||||
const totalOffers = ref(0)
|
||||
const isLoadingMore = ref(false)
|
||||
|
||||
@@ -164,7 +166,7 @@ const {
|
||||
|
||||
watchEffect(() => {
|
||||
if (offersData.value?.getOffers) {
|
||||
offers.value = offersData.value.getOffers
|
||||
offers.value = offersData.value.getOffers.filter((o): o is Offer => o !== null)
|
||||
totalOffers.value = offersData.value.getOffersCount ?? offersData.value.getOffers.length
|
||||
}
|
||||
})
|
||||
@@ -231,9 +233,11 @@ const onSearch = () => {
|
||||
// TODO: Implement search
|
||||
}
|
||||
|
||||
const onSelectOffer = (offer: any) => {
|
||||
selectedOfferId.value = offer.uuid
|
||||
navigateTo(localePath(`/clientarea/offers/${offer.uuid}`))
|
||||
const onSelectOffer = (offer: { uuid?: string | null }) => {
|
||||
if (offer.uuid) {
|
||||
selectedOfferId.value = offer.uuid
|
||||
navigateTo(localePath(`/clientarea/offers/${offer.uuid}`))
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusVariant = (status: string) => {
|
||||
@@ -293,7 +297,7 @@ const fetchOffers = async (offset = 0, replace = false) => {
|
||||
'public',
|
||||
'exchange'
|
||||
)
|
||||
const next = data?.getOffers || []
|
||||
const next = (data?.getOffers || []).filter((o): o is Offer => o !== null)
|
||||
offers.value = replace ? next : offers.value.concat(next)
|
||||
totalOffers.value = data?.getOffersCount ?? totalOffers.value
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<template v-else>
|
||||
<Grid v-if="products.length" :cols="1" :md="2" :lg="3" :gap="4">
|
||||
<Card
|
||||
v-for="product in products"
|
||||
:key="product.uuid"
|
||||
v-for="(product, index) in products"
|
||||
:key="product.uuid ?? index"
|
||||
padding="lg"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
@click="selectProduct(product)"
|
||||
@@ -51,7 +51,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
|
||||
import { GetProductsDocument, type GetProductsQueryResult } from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
type Product = NonNullable<NonNullable<GetProductsQueryResult['getProducts']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
@@ -62,7 +64,7 @@ const localePath = useLocalePath()
|
||||
const { t } = useI18n()
|
||||
const { execute } = useGraphQL()
|
||||
|
||||
const products = ref<any[]>([])
|
||||
const products = ref<Product[]>([])
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
const error = ref('')
|
||||
@@ -73,19 +75,19 @@ const loadProducts = async () => {
|
||||
hasError.value = false
|
||||
const { data, error: productsError } = await useServerQuery('offers-new-products', GetProductsDocument, {}, 'public', 'exchange')
|
||||
if (productsError.value) throw productsError.value
|
||||
products.value = data.value?.getProducts || []
|
||||
} catch (err: any) {
|
||||
products.value = (data.value?.getProducts || []).filter((p): p is Product => p !== null)
|
||||
} catch (err: unknown) {
|
||||
hasError.value = true
|
||||
error.value = err.message || t('offersNew.errors.load_failed')
|
||||
error.value = err instanceof Error ? err.message : t('offersNew.errors.load_failed')
|
||||
products.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectProduct = (product: any) => {
|
||||
const selectProduct = (product: { uuid?: string | null }) => {
|
||||
// Navigate to product details page
|
||||
navigateTo(localePath(`/clientarea/offers/${product.uuid}`))
|
||||
if (product.uuid) navigateTo(localePath(`/clientarea/offers/${product.uuid}`))
|
||||
}
|
||||
|
||||
await loadProducts()
|
||||
|
||||
@@ -46,9 +46,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetOrderDocument } from '~/composables/graphql/team/orders-generated'
|
||||
import { GetOrderDocument, type GetOrderQueryResult } from '~/composables/graphql/team/orders-generated'
|
||||
import type { RouteStageItem } from '~/components/RouteStagesList.vue'
|
||||
|
||||
// Types from GraphQL
|
||||
type OrderType = NonNullable<GetOrderQueryResult['getOrder']>
|
||||
type StageType = NonNullable<NonNullable<OrderType['stages']>[number]>
|
||||
type TripType = NonNullable<NonNullable<StageType['trips']>[number]>
|
||||
type CompanyType = NonNullable<StageType['selectedCompany']>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
middleware: ['auth-oidc']
|
||||
@@ -57,7 +63,7 @@ definePageMeta({
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const order = ref<any>(null)
|
||||
const order = ref<OrderType | null>(null)
|
||||
const isLoadingOrder = ref(true)
|
||||
const hasOrderError = ref(false)
|
||||
const orderError = ref('')
|
||||
@@ -96,8 +102,8 @@ const orderMeta = computed(() => {
|
||||
|
||||
const orderRoutesForMap = computed(() => {
|
||||
const stages = (order.value?.stages || [])
|
||||
.filter(Boolean)
|
||||
.map((stage: any) => {
|
||||
.filter((stage): stage is StageType => stage !== null)
|
||||
.map((stage) => {
|
||||
if (stage.stageType === 'transport') {
|
||||
if (!stage.sourceLatitude || !stage.sourceLongitude || !stage.destinationLatitude || !stage.destinationLongitude) return null
|
||||
return {
|
||||
@@ -118,33 +124,43 @@ const orderRoutesForMap = computed(() => {
|
||||
return [{ stages }]
|
||||
})
|
||||
|
||||
// Company summary type
|
||||
interface CompanySummary {
|
||||
name: string | null | undefined
|
||||
totalWeight: number
|
||||
tripsCount: number
|
||||
company: CompanyType | null | undefined
|
||||
}
|
||||
|
||||
const orderStageItems = computed<RouteStageItem[]>(() => {
|
||||
return (order.value?.stages || []).map((stage: any) => {
|
||||
const isTransport = stage.stageType === 'transport'
|
||||
const from = isTransport ? stage.sourceLocationName : stage.locationName
|
||||
const to = isTransport ? stage.destinationLocationName : stage.locationName
|
||||
return (order.value?.stages || [])
|
||||
.filter((stage): stage is StageType => stage !== null)
|
||||
.map((stage) => {
|
||||
const isTransport = stage.stageType === 'transport'
|
||||
const from = isTransport ? stage.sourceLocationName : stage.locationName
|
||||
const to = isTransport ? stage.destinationLocationName : stage.locationName
|
||||
|
||||
const meta: string[] = []
|
||||
const dateRange = getStageDateRange(stage)
|
||||
if (dateRange) {
|
||||
meta.push(dateRange)
|
||||
}
|
||||
const meta: string[] = []
|
||||
const dateRange = getStageDateRange(stage)
|
||||
if (dateRange) {
|
||||
meta.push(dateRange)
|
||||
}
|
||||
|
||||
const companies = getCompaniesSummary(stage)
|
||||
companies.forEach((company: any) => {
|
||||
meta.push(
|
||||
`${company.name} · ${company.totalWeight || 0}${t('ordersDetail.labels.weight_unit')} · ${company.tripsCount || 0} ${t('ordersDetail.labels.trips')}`
|
||||
)
|
||||
const companies = getCompaniesSummary(stage)
|
||||
companies.forEach((company: CompanySummary) => {
|
||||
meta.push(
|
||||
`${company.name} · ${company.totalWeight || 0}${t('ordersDetail.labels.weight_unit')} · ${company.tripsCount || 0} ${t('ordersDetail.labels.trips')}`
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
key: stage.uuid ?? undefined,
|
||||
from: from ?? undefined,
|
||||
to: to ?? undefined,
|
||||
label: stage.name ?? undefined,
|
||||
meta
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
key: stage.uuid,
|
||||
from,
|
||||
to,
|
||||
label: stage.name,
|
||||
meta
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const loadOrder = async () => {
|
||||
@@ -154,10 +170,10 @@ const loadOrder = async () => {
|
||||
const orderUuid = route.params.id as string
|
||||
const { data, error: orderErrorResp } = await useServerQuery('order-detail', GetOrderDocument, { orderUuid }, 'team', 'orders')
|
||||
if (orderErrorResp.value) throw orderErrorResp.value
|
||||
order.value = data.value?.getOrder
|
||||
} catch (err: any) {
|
||||
order.value = data.value?.getOrder ?? null
|
||||
} catch (err: unknown) {
|
||||
hasOrderError.value = true
|
||||
orderError.value = err.message || t('ordersDetail.errors.load_failed')
|
||||
orderError.value = err instanceof Error ? err.message : t('ordersDetail.errors.load_failed')
|
||||
} finally {
|
||||
isLoadingOrder.value = false
|
||||
}
|
||||
@@ -172,8 +188,8 @@ const formatPrice = (price: number, currency?: string | null) => {
|
||||
}).format(price)
|
||||
}
|
||||
|
||||
const getCompaniesSummary = (stage: any) => {
|
||||
const companies = []
|
||||
const getCompaniesSummary = (stage: StageType): CompanySummary[] => {
|
||||
const companies: CompanySummary[] = []
|
||||
if (stage.stageType === 'service' && stage.selectedCompany) {
|
||||
companies.push({
|
||||
name: stage.selectedCompany.name,
|
||||
@@ -185,12 +201,13 @@ const getCompaniesSummary = (stage: any) => {
|
||||
}
|
||||
|
||||
if (stage.stageType === 'transport' && stage.trips?.length) {
|
||||
const companiesMap = new Map()
|
||||
stage.trips.forEach((trip: any) => {
|
||||
const companiesMap = new Map<string, CompanySummary>()
|
||||
stage.trips.forEach((trip) => {
|
||||
if (!trip) return
|
||||
const companyName = trip.company?.name || t('ordersDetail.labels.company_unknown')
|
||||
const weight = trip.plannedWeight || 0
|
||||
if (companiesMap.has(companyName)) {
|
||||
const existing = companiesMap.get(companyName)
|
||||
const existing = companiesMap.get(companyName)!
|
||||
existing.totalWeight += weight
|
||||
existing.tripsCount += 1
|
||||
} else {
|
||||
@@ -211,10 +228,12 @@ const getOrderDuration = () => {
|
||||
if (!order.value?.stages?.length) return 0
|
||||
let minDate: Date | null = null
|
||||
let maxDate: Date | null = null
|
||||
order.value.stages.forEach((stage: any) => {
|
||||
stage.trips?.forEach((trip: any) => {
|
||||
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate)
|
||||
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate)
|
||||
order.value.stages.forEach((stage) => {
|
||||
if (!stage) return
|
||||
stage.trips?.forEach((trip) => {
|
||||
if (!trip) return
|
||||
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate || '')
|
||||
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate || '')
|
||||
if (!minDate || startDate < minDate) minDate = startDate
|
||||
if (!maxDate || endDate > maxDate) maxDate = endDate
|
||||
})
|
||||
@@ -224,13 +243,14 @@ const getOrderDuration = () => {
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
const getStageDateRange = (stage: any) => {
|
||||
const getStageDateRange = (stage: StageType) => {
|
||||
if (!stage.trips?.length) return t('ordersDetail.labels.dates_undefined')
|
||||
let minDate: Date | null = null
|
||||
let maxDate: Date | null = null
|
||||
stage.trips.forEach((trip: any) => {
|
||||
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate)
|
||||
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate)
|
||||
stage.trips.forEach((trip) => {
|
||||
if (!trip) return
|
||||
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate || '')
|
||||
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate || '')
|
||||
if (!minDate || startDate < minDate) minDate = startDate
|
||||
if (!maxDate || endDate > maxDate) maxDate = endDate
|
||||
})
|
||||
|
||||
@@ -100,6 +100,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
|
||||
import type { GetTeamOrdersQueryResult } from '~/composables/graphql/team/orders-generated'
|
||||
|
||||
type TeamOrder = NonNullable<NonNullable<GetTeamOrdersQueryResult['getTeamOrders']>[number]>
|
||||
type TeamOrderStage = NonNullable<NonNullable<TeamOrder['stages']>[number]>
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav',
|
||||
@@ -131,19 +135,28 @@ const currentBounds = ref<MapBounds | null>(null)
|
||||
|
||||
// List items - one per order
|
||||
const listItems = computed(() => {
|
||||
return filteredItems.value.map(order => ({
|
||||
...order,
|
||||
uuid: order.uuid,
|
||||
name: order.name || `#${order.uuid.slice(0, 8)}`,
|
||||
latitude: order.sourceLatitude,
|
||||
longitude: order.sourceLongitude,
|
||||
country: order.sourceLocationName
|
||||
}))
|
||||
return filteredItems.value
|
||||
.filter(order => order.uuid)
|
||||
.map(order => ({
|
||||
...order,
|
||||
uuid: order.uuid,
|
||||
name: order.name || `#${order.uuid!.slice(0, 8)}`,
|
||||
latitude: order.sourceLatitude,
|
||||
longitude: order.sourceLongitude,
|
||||
country: order.sourceLocationName
|
||||
}))
|
||||
})
|
||||
|
||||
// Map points - two per order (source + destination)
|
||||
interface MapPoint {
|
||||
uuid: string
|
||||
name: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
|
||||
const mapPoints = computed(() => {
|
||||
const result: any[] = []
|
||||
const result: MapPoint[] = []
|
||||
filteredItems.value.forEach(order => {
|
||||
// Source point
|
||||
if (order.sourceLatitude && order.sourceLongitude) {
|
||||
@@ -202,22 +215,26 @@ const onSearch = () => {
|
||||
// TODO: Implement search
|
||||
}
|
||||
|
||||
const onSelectOrder = (item: any) => {
|
||||
selectedOrderId.value = item.uuid
|
||||
navigateTo(localePath(`/clientarea/orders/${item.uuid}`))
|
||||
const onSelectOrder = (item: { uuid?: string | null }) => {
|
||||
if (item.uuid) {
|
||||
selectedOrderId.value = item.uuid
|
||||
navigateTo(localePath(`/clientarea/orders/${item.uuid}`))
|
||||
}
|
||||
}
|
||||
|
||||
await init()
|
||||
|
||||
const getOrderStartDate = (order: any) => {
|
||||
const getOrderStartDate = (order: TeamOrder) => {
|
||||
if (!order.createdAt) return t('ordersDetail.labels.dates_undefined')
|
||||
return formatDate(order.createdAt)
|
||||
}
|
||||
|
||||
const getOrderEndDate = (order: any) => {
|
||||
const getOrderEndDate = (order: TeamOrder) => {
|
||||
let latestDate: Date | null = null
|
||||
order.stages?.forEach((stage: any) => {
|
||||
stage.trips?.forEach((trip: any) => {
|
||||
order.stages?.forEach((stage) => {
|
||||
if (!stage) return
|
||||
stage.trips?.forEach((trip) => {
|
||||
if (!trip) return
|
||||
const endDate = trip.actualUnloadingDate || trip.plannedUnloadingDate
|
||||
if (endDate) {
|
||||
const date = new Date(endDate)
|
||||
@@ -236,9 +253,10 @@ const getOrderEndDate = (order: any) => {
|
||||
return t('ordersDetail.labels.dates_undefined')
|
||||
}
|
||||
|
||||
const getCompletedStages = (order: any) => {
|
||||
const getCompletedStages = (order: TeamOrder) => {
|
||||
if (!order.stages?.length) return 0
|
||||
return order.stages.filter((stage: any) => stage.status === 'completed').length
|
||||
// Note: StageType doesn't have a status field, count all stages for now
|
||||
return order.stages.filter((stage): stage is TeamOrderStage => stage !== null).length
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
>
|
||||
<template #cards>
|
||||
<Card
|
||||
v-for="order in filteredItems"
|
||||
:key="order.uuid"
|
||||
v-for="(order, index) in filteredItems"
|
||||
:key="order.uuid ?? index"
|
||||
padding="small"
|
||||
interactive
|
||||
:class="{ 'ring-2 ring-primary': selectedOrderId === order.uuid }"
|
||||
@@ -24,8 +24,8 @@
|
||||
<Stack gap="2">
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Text weight="semibold">#{{ order.name }}</Text>
|
||||
<Badge :variant="getStatusVariant(order.status)" size="sm">
|
||||
{{ getStatusText(order.status) }}
|
||||
<Badge :variant="getStatusVariant(order.status || '')" size="sm">
|
||||
{{ getStatusText(order.status || '') }}
|
||||
</Badge>
|
||||
</Stack>
|
||||
<Text tone="muted" size="sm" class="truncate">
|
||||
@@ -74,9 +74,11 @@ await init()
|
||||
const mapRef = ref<{ flyTo: (orderId: string) => void } | null>(null)
|
||||
const selectedOrderId = ref<string | null>(null)
|
||||
|
||||
const selectOrder = (order: any) => {
|
||||
selectedOrderId.value = order.uuid
|
||||
mapRef.value?.flyTo(order.uuid)
|
||||
const selectOrder = (order: { uuid?: string | null }) => {
|
||||
if (order.uuid) {
|
||||
selectedOrderId.value = order.uuid
|
||||
mapRef.value?.flyTo(order.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
const onMapSelectOrder = (uuid: string) => {
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
<Heading :level="2">{{ t('clientTeam.members.title') }}</Heading>
|
||||
<Grid :cols="1" :md="2" :lg="3" :gap="4">
|
||||
<Card
|
||||
v-for="member in currentTeam?.members || []"
|
||||
:key="member.user?.id"
|
||||
v-for="(member, index) in currentTeamMembers"
|
||||
:key="member.user?.id ?? `member-${index}`"
|
||||
padding="lg"
|
||||
>
|
||||
<Stack gap="3">
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<!-- Pending invitations -->
|
||||
<Card
|
||||
v-for="invitation in currentTeam?.invitations || []"
|
||||
v-for="invitation in currentTeamInvitations"
|
||||
:key="invitation.uuid"
|
||||
padding="lg"
|
||||
class="border-dashed border-warning"
|
||||
@@ -111,7 +111,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetTeamDocument } from '~/composables/graphql/user/teams-generated'
|
||||
import { GetTeamDocument, type GetTeamQueryResult } from '~/composables/graphql/user/teams-generated'
|
||||
|
||||
interface UserTeam {
|
||||
id?: string | null
|
||||
name: string
|
||||
logtoOrgId?: string | null
|
||||
}
|
||||
|
||||
type TeamWithMembers = NonNullable<GetTeamQueryResult['getTeam']>
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -129,8 +137,8 @@ const me = useState<{
|
||||
} | null>('me', () => null)
|
||||
const { setActiveTeam } = useActiveTeam()
|
||||
|
||||
const userTeams = ref<any[]>([])
|
||||
const currentTeam = ref<any>(null)
|
||||
const userTeams = ref<UserTeam[]>([])
|
||||
const currentTeam = ref<TeamWithMembers | UserTeam | null>(null)
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
const error = ref('')
|
||||
@@ -143,7 +151,7 @@ const teamHeaderActions = computed(() => {
|
||||
}
|
||||
return actions
|
||||
})
|
||||
const roleText = (role?: string) => {
|
||||
const roleText = (role?: string | null) => {
|
||||
const map: Record<string, string> = {
|
||||
OWNER: t('clientTeam.roles.owner'),
|
||||
ADMIN: t('clientTeam.roles.admin'),
|
||||
@@ -153,13 +161,30 @@ const roleText = (role?: string) => {
|
||||
return map[role || ''] || role || t('clientTeam.roles.member')
|
||||
}
|
||||
|
||||
const getMemberInitials = (user?: any) => {
|
||||
interface TeamMember {
|
||||
id?: string | null
|
||||
firstName?: string | null
|
||||
lastName?: string | null
|
||||
}
|
||||
|
||||
const getMemberInitials = (user?: TeamMember | null) => {
|
||||
if (!user) return '??'
|
||||
const first = user.firstName?.charAt(0) || ''
|
||||
const last = user.lastName?.charAt(0) || ''
|
||||
return (first + last).toUpperCase() || user.id?.charAt(0).toUpperCase() || '??'
|
||||
}
|
||||
|
||||
// Type-safe accessors for TeamWithMembers properties
|
||||
const currentTeamMembers = computed(() => {
|
||||
const team = currentTeam.value
|
||||
return team && 'members' in team ? (team.members || []).filter((m): m is NonNullable<typeof m> => m !== null) : []
|
||||
})
|
||||
|
||||
const currentTeamInvitations = computed(() => {
|
||||
const team = currentTeam.value
|
||||
return team && 'invitations' in team ? (team.invitations || []).filter((i): i is NonNullable<typeof i> => i !== null) : []
|
||||
})
|
||||
|
||||
const loadUserTeams = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
@@ -177,13 +202,15 @@ const loadUserTeams = async () => {
|
||||
currentTeam.value = teamData.value?.getTeam || null
|
||||
} else if (userTeams.value.length > 0) {
|
||||
const firstTeam = userTeams.value[0]
|
||||
setActiveTeam(firstTeam?.id || null, firstTeam?.logtoOrgId)
|
||||
currentTeam.value = firstTeam
|
||||
if (firstTeam) {
|
||||
setActiveTeam(firstTeam.id || null, firstTeam.logtoOrgId)
|
||||
currentTeam.value = firstTeam
|
||||
}
|
||||
}
|
||||
// Если нет команды - currentTeam остаётся null, показываем EmptyState
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
hasError.value = true
|
||||
error.value = err.message || t('clientTeam.error.load')
|
||||
error.value = err instanceof Error ? err.message : t('clientTeam.error.load')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ const submitInvite = async () => {
|
||||
} else {
|
||||
inviteError.value = result?.inviteMember?.message || t('clientTeam.invite.error')
|
||||
}
|
||||
} catch (err: any) {
|
||||
inviteError.value = err.message || t('clientTeam.invite.error')
|
||||
} catch (err: unknown) {
|
||||
inviteError.value = err instanceof Error ? err.message : t('clientTeam.invite.error')
|
||||
} finally {
|
||||
inviteLoading.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user