feat(catalog): KYC bottom sheet instead of separate page
All checks were successful
Build Docker Image / build (push) Successful in 4m21s
All checks were successful
Build Docker Image / build (push) Successful in 4m21s
- Add KycBottomSheet component with glass effect (70vh height) - Animate sheet sliding up from bottom when opening KYC - InfoPanel hides when KYC sheet is open - Click outside or X button to close - Contains all company info: реквизиты, руководство, учредители, контакты, финансы, арбитраж, ОКВЭД
This commit is contained in:
304
app/components/catalog/KycBottomSheet.vue
Normal file
304
app/components/catalog/KycBottomSheet.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<Transition name="kyc-slide">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-x-0 bottom-0 z-50 flex flex-col"
|
||||
style="height: 70vh"
|
||||
>
|
||||
<!-- Backdrop (clickable to close) -->
|
||||
<div
|
||||
class="absolute inset-0 -top-[30vh] bg-black/30"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
|
||||
<!-- Sheet content -->
|
||||
<div class="relative flex-1 bg-black/40 backdrop-blur-xl rounded-t-2xl border-t border-white/20 shadow-2xl overflow-hidden">
|
||||
<!-- Header with drag handle and close -->
|
||||
<div class="sticky top-0 z-10 bg-black/30 backdrop-blur-md border-b border-white/10">
|
||||
<div class="flex justify-center py-2">
|
||||
<div class="w-12 h-1.5 bg-white/30 rounded-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-6 pb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary/20 rounded-xl flex items-center justify-center">
|
||||
<Icon name="lucide:building-2" size="24" class="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<Text weight="bold" size="lg" class="text-white">{{ companyName }}</Text>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span class="badge badge-success badge-sm">{{ $t('catalog.info.active') }}</span>
|
||||
<span class="badge badge-outline badge-sm text-white/60">{{ companyType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-circle text-white/60 hover:text-white" @click="emit('close')">
|
||||
<Icon name="lucide:x" size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable content -->
|
||||
<div class="overflow-y-auto h-[calc(70vh-100px)] px-6 py-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Left Column -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Реквизиты -->
|
||||
<div class="bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<Text weight="semibold" class="text-white mb-3 flex items-center gap-2">
|
||||
<Icon name="lucide:file-text" size="18" />
|
||||
Реквизиты
|
||||
</Text>
|
||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<Text tone="muted" size="xs" class="text-white/50">ИНН</Text>
|
||||
<Text class="text-white font-mono">{{ inn }}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text tone="muted" size="xs" class="text-white/50">КПП</Text>
|
||||
<Text class="text-white font-mono">{{ kpp }}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text tone="muted" size="xs" class="text-white/50">ОГРН</Text>
|
||||
<Text class="text-white font-mono">{{ ogrn }}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text tone="muted" size="xs" class="text-white/50">Год регистрации</Text>
|
||||
<Text class="text-white">{{ registrationYear }}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Руководство -->
|
||||
<div class="bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<Text weight="semibold" class="text-white mb-3 flex items-center gap-2">
|
||||
<Icon name="lucide:user-cog" size="18" />
|
||||
Руководство
|
||||
</Text>
|
||||
<div class="flex items-center gap-3 p-2 bg-white/5 rounded-lg">
|
||||
<div class="avatar placeholder">
|
||||
<div class="w-9 h-9 rounded-full bg-primary text-primary-content text-sm">
|
||||
<span>{{ directorInitials }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text weight="medium" size="sm" class="text-white">{{ directorName }}</Text>
|
||||
<Text size="xs" class="text-white/50">Генеральный директор</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Учредители -->
|
||||
<div class="bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<Text weight="semibold" class="text-white mb-3 flex items-center gap-2">
|
||||
<Icon name="lucide:users" size="18" />
|
||||
Учредители
|
||||
</Text>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(founder, i) in founders"
|
||||
:key="i"
|
||||
class="flex items-center justify-between p-2 bg-white/5 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="avatar placeholder">
|
||||
<div class="w-8 h-8 rounded-full bg-secondary text-secondary-content text-xs">
|
||||
<span>{{ founder.initials }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" class="text-white">{{ founder.name }}</Text>
|
||||
<Text size="xs" class="text-white/50">Физ. лицо</Text>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge badge-primary badge-sm">{{ founder.share }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t border-white/10 flex justify-between">
|
||||
<Text size="xs" class="text-white/50">Уставный капитал</Text>
|
||||
<Text weight="semibold" size="sm" class="text-white">{{ authorizedCapital }}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Контакты -->
|
||||
<div class="bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<Text weight="semibold" class="text-white mb-3 flex items-center gap-2">
|
||||
<Icon name="lucide:contact" size="18" />
|
||||
Контакты
|
||||
</Text>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex items-center gap-2 text-white/80">
|
||||
<Icon name="lucide:map-pin" size="14" class="text-white/50" />
|
||||
<span>{{ address }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-white/80">
|
||||
<Icon name="lucide:phone" size="14" class="text-white/50" />
|
||||
<span>{{ phone }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-white/80">
|
||||
<Icon name="lucide:mail" size="14" class="text-white/50" />
|
||||
<span>{{ email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Финансы -->
|
||||
<div class="bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<Text weight="semibold" class="text-white mb-3 flex items-center gap-2">
|
||||
<Icon name="lucide:bar-chart-3" size="18" />
|
||||
Финансы (2024)
|
||||
</Text>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="flex justify-between mb-1">
|
||||
<Text size="xs" class="text-white/50">Выручка</Text>
|
||||
<Text size="xs" class="text-success">↑ 15%</Text>
|
||||
</div>
|
||||
<Text weight="bold" class="text-white">{{ revenue }}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between mb-1">
|
||||
<Text size="xs" class="text-white/50">Чистая прибыль</Text>
|
||||
<Text size="xs" class="text-success">↑ 23%</Text>
|
||||
</div>
|
||||
<Text weight="bold" class="text-white">{{ profit }}</Text>
|
||||
</div>
|
||||
<div class="pt-2 border-t border-white/10 flex justify-between">
|
||||
<Text size="xs" class="text-white/50">Сотрудников</Text>
|
||||
<Text weight="medium" size="sm" class="text-white">{{ employees }}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Арбитраж -->
|
||||
<div class="bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<Text weight="semibold" class="text-white mb-3 flex items-center gap-2">
|
||||
<Icon name="lucide:scale" size="18" />
|
||||
Арбитражные дела
|
||||
</Text>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="badge badge-warning badge-xs">Истец</span>
|
||||
<Text class="text-white/80">{{ arbitration.plaintiff.count }} дела</Text>
|
||||
</div>
|
||||
<Text class="text-white">{{ arbitration.plaintiff.amount }}</Text>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="badge badge-error badge-xs">Ответчик</span>
|
||||
<Text class="text-white/80">{{ arbitration.defendant.count }} дело</Text>
|
||||
</div>
|
||||
<Text class="text-white">{{ arbitration.defendant.amount }}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ОКВЭД (full width) -->
|
||||
<div class="mt-4 bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<Text weight="semibold" class="text-white mb-3 flex items-center gap-2">
|
||||
<Icon name="lucide:briefcase" size="18" />
|
||||
Виды деятельности (ОКВЭД)
|
||||
</Text>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-start gap-2 p-2 bg-primary/10 rounded-lg border border-primary/20">
|
||||
<span class="badge badge-primary badge-xs mt-0.5">Осн.</span>
|
||||
<Text size="sm" class="text-white">{{ mainActivity }}</Text>
|
||||
</div>
|
||||
<div
|
||||
v-for="(activity, i) in additionalActivities"
|
||||
:key="i"
|
||||
class="flex items-start gap-2 p-2 bg-white/5 rounded-lg"
|
||||
>
|
||||
<span class="badge badge-ghost badge-xs mt-0.5 text-white/50">Доп.</span>
|
||||
<Text size="sm" class="text-white/80">{{ activity }}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sources footer -->
|
||||
<div class="mt-4 flex items-center justify-between text-xs text-white/40 px-1">
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="lucide:database" size="12" />
|
||||
Источники: ЕГРЮЛ, ФНС, Росстат
|
||||
</span>
|
||||
<span>Обновлено: {{ lastUpdated }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Demo notice -->
|
||||
<div class="mt-4 alert bg-info/20 border border-info/30 text-info text-sm">
|
||||
<Icon name="lucide:info" size="16" />
|
||||
<span>{{ $t('kyc.demo.notice') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
isOpen: boolean
|
||||
uuid: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'close': []
|
||||
}>()
|
||||
|
||||
// Demo data (will be replaced with real data from API)
|
||||
const isDemo = computed(() => props.uuid === 'demo-kyc-profile')
|
||||
|
||||
const companyName = computed(() => isDemo.value ? 'ООО "АГРОТОРГ ПЛЮС"' : 'Загрузка...')
|
||||
const companyType = computed(() => 'ООО')
|
||||
const inn = computed(() => '7707456789')
|
||||
const kpp = computed(() => '770701001')
|
||||
const ogrn = computed(() => '1157746123456')
|
||||
const registrationYear = computed(() => '2015')
|
||||
const directorName = computed(() => 'Петров Сергей Александрович')
|
||||
const directorInitials = computed(() => 'ПС')
|
||||
const founders = computed(() => [
|
||||
{ name: 'Петров Сергей Александрович', initials: 'ПС', share: 60 },
|
||||
{ name: 'Иванова Анна Петровна', initials: 'ИА', share: 40 }
|
||||
])
|
||||
const authorizedCapital = computed(() => '500 000 ₽')
|
||||
const address = computed(() => 'г. Москва, ул. Складская, д. 15, оф. 301')
|
||||
const phone = computed(() => '+7 (495) 123-45-67')
|
||||
const email = computed(() => 'info@agrotorg-plus.ru')
|
||||
const revenue = computed(() => '245 800 000 ₽')
|
||||
const profit = computed(() => '18 450 000 ₽')
|
||||
const employees = computed(() => '47 человек')
|
||||
const arbitration = computed(() => ({
|
||||
plaintiff: { count: 3, amount: '1 250 000 ₽' },
|
||||
defendant: { count: 1, amount: '320 000 ₽' }
|
||||
}))
|
||||
const mainActivity = computed(() => '46.21 - Торговля оптовая зерном, семенами и кормами')
|
||||
const additionalActivities = computed(() => [
|
||||
'46.11 - Деятельность агентов по оптовой торговле',
|
||||
'52.10 - Деятельность по складированию и хранению'
|
||||
])
|
||||
const lastUpdated = computed(() => new Date().toLocaleDateString('ru-RU'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kyc-slide-enter-active,
|
||||
.kyc-slide-leave-active {
|
||||
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.kyc-slide-enter-from,
|
||||
.kyc-slide-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.kyc-slide-enter-to,
|
||||
.kyc-slide-leave-from {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,72 +1,81 @@
|
||||
<template>
|
||||
<CatalogPage
|
||||
ref="catalogPageRef"
|
||||
:loading="isLoading"
|
||||
:use-server-clustering="true"
|
||||
:cluster-node-type="clusterNodeType"
|
||||
map-id="unified-catalog-map"
|
||||
:point-color="mapPointColor"
|
||||
:items="currentSelectionItems"
|
||||
:hovered-id="hoveredItemId ?? undefined"
|
||||
:show-panel="showPanel"
|
||||
:filter-by-bounds="filterByBounds"
|
||||
:related-points="relatedPoints"
|
||||
:info-loading="isInfoLoading"
|
||||
@select="onMapSelect"
|
||||
@bounds-change="onBoundsChange"
|
||||
@update:filter-by-bounds="$event ? setBoundsInUrl(currentMapBounds) : clearBoundsFromUrl()"
|
||||
>
|
||||
<!-- Panel slot - shows selection list OR info OR quote results -->
|
||||
<template #panel>
|
||||
<!-- Selection mode: show list for picking product/hub/supplier -->
|
||||
<SelectionPanel
|
||||
v-if="selectMode"
|
||||
:select-mode="selectMode"
|
||||
:products="filteredProducts"
|
||||
:hubs="filteredHubs"
|
||||
:suppliers="filteredSuppliers"
|
||||
:loading="selectionLoading"
|
||||
:loading-more="selectionLoadingMore"
|
||||
:has-more="selectionHasMore && !filterByBounds"
|
||||
@select="onSelectItem"
|
||||
@close="onClosePanel"
|
||||
@load-more="onLoadMore"
|
||||
@hover="onHoverItem"
|
||||
/>
|
||||
<div>
|
||||
<CatalogPage
|
||||
ref="catalogPageRef"
|
||||
:loading="isLoading"
|
||||
:use-server-clustering="true"
|
||||
:cluster-node-type="clusterNodeType"
|
||||
map-id="unified-catalog-map"
|
||||
:point-color="mapPointColor"
|
||||
:items="currentSelectionItems"
|
||||
:hovered-id="hoveredItemId ?? undefined"
|
||||
:show-panel="showPanel && !kycSheetUuid"
|
||||
:filter-by-bounds="filterByBounds"
|
||||
:related-points="relatedPoints"
|
||||
:info-loading="isInfoLoading"
|
||||
@select="onMapSelect"
|
||||
@bounds-change="onBoundsChange"
|
||||
@update:filter-by-bounds="$event ? setBoundsInUrl(currentMapBounds) : clearBoundsFromUrl()"
|
||||
>
|
||||
<!-- Panel slot - shows selection list OR info OR quote results -->
|
||||
<template #panel>
|
||||
<!-- Selection mode: show list for picking product/hub/supplier -->
|
||||
<SelectionPanel
|
||||
v-if="selectMode"
|
||||
:select-mode="selectMode"
|
||||
:products="filteredProducts"
|
||||
:hubs="filteredHubs"
|
||||
:suppliers="filteredSuppliers"
|
||||
:loading="selectionLoading"
|
||||
:loading-more="selectionLoadingMore"
|
||||
:has-more="selectionHasMore && !filterByBounds"
|
||||
@select="onSelectItem"
|
||||
@close="onClosePanel"
|
||||
@load-more="onLoadMore"
|
||||
@hover="onHoverItem"
|
||||
/>
|
||||
|
||||
<!-- Info mode: show detailed info about selected entity -->
|
||||
<InfoPanel
|
||||
v-else-if="infoId"
|
||||
:entity-type="infoId.type"
|
||||
:entity-id="infoId.uuid"
|
||||
:entity="entity"
|
||||
:related-products="relatedProducts"
|
||||
:related-hubs="relatedHubs"
|
||||
:related-suppliers="relatedSuppliers"
|
||||
:related-offers="relatedOffers"
|
||||
:selected-product="infoProduct ?? null"
|
||||
:loading="infoLoading"
|
||||
:loading-products="isLoadingProducts"
|
||||
:loading-hubs="isLoadingHubs"
|
||||
:loading-suppliers="isLoadingSuppliers"
|
||||
:loading-offers="isLoadingOffers"
|
||||
@close="onInfoClose"
|
||||
@add-to-filter="onInfoAddToFilter"
|
||||
@open-info="onInfoOpenRelated"
|
||||
@select-product="onInfoSelectProduct"
|
||||
@select-offer="onSelectOffer"
|
||||
@open-kyc="onOpenKyc"
|
||||
/>
|
||||
<!-- Info mode: show detailed info about selected entity -->
|
||||
<InfoPanel
|
||||
v-else-if="infoId"
|
||||
:entity-type="infoId.type"
|
||||
:entity-id="infoId.uuid"
|
||||
:entity="entity"
|
||||
:related-products="relatedProducts"
|
||||
:related-hubs="relatedHubs"
|
||||
:related-suppliers="relatedSuppliers"
|
||||
:related-offers="relatedOffers"
|
||||
:selected-product="infoProduct ?? null"
|
||||
:loading="infoLoading"
|
||||
:loading-products="isLoadingProducts"
|
||||
:loading-hubs="isLoadingHubs"
|
||||
:loading-suppliers="isLoadingSuppliers"
|
||||
:loading-offers="isLoadingOffers"
|
||||
@close="onInfoClose"
|
||||
@add-to-filter="onInfoAddToFilter"
|
||||
@open-info="onInfoOpenRelated"
|
||||
@select-product="onInfoSelectProduct"
|
||||
@select-offer="onSelectOffer"
|
||||
@open-kyc="onOpenKyc"
|
||||
/>
|
||||
|
||||
<!-- Quote results: show offers after search -->
|
||||
<QuotePanel
|
||||
v-else-if="showQuoteResults"
|
||||
:loading="offersLoading"
|
||||
:offers="offers"
|
||||
@select-offer="onSelectOffer"
|
||||
/>
|
||||
</template>
|
||||
</CatalogPage>
|
||||
<!-- Quote results: show offers after search -->
|
||||
<QuotePanel
|
||||
v-else-if="showQuoteResults"
|
||||
:loading="offersLoading"
|
||||
:offers="offers"
|
||||
@select-offer="onSelectOffer"
|
||||
/>
|
||||
</template>
|
||||
</CatalogPage>
|
||||
|
||||
<!-- KYC Bottom Sheet (overlays everything) -->
|
||||
<KycBottomSheet
|
||||
:is-open="!!kycSheetUuid"
|
||||
:uuid="kycSheetUuid"
|
||||
@close="onCloseKycSheet"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -495,10 +504,18 @@ const onInfoSelectProduct = (uuid: string | null) => {
|
||||
setInfoProduct(uuid)
|
||||
}
|
||||
|
||||
// Handle KYC profile open - navigate to KYC page
|
||||
// KYC Bottom Sheet state
|
||||
const kycSheetUuid = ref<string | null>(null)
|
||||
|
||||
// Handle KYC profile open - show bottom sheet instead of navigating
|
||||
const onOpenKyc = (uuid: string | undefined) => {
|
||||
if (!uuid) return
|
||||
navigateTo(localePath(`/kyc/${uuid}`))
|
||||
kycSheetUuid.value = uuid
|
||||
}
|
||||
|
||||
// Close KYC bottom sheet
|
||||
const onCloseKycSheet = () => {
|
||||
kycSheetUuid.value = null
|
||||
}
|
||||
|
||||
// Search for offers
|
||||
|
||||
Reference in New Issue
Block a user