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

@@ -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
}

View File

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