diff --git a/app/composables/useIncrementalList.ts b/app/composables/useIncrementalList.ts new file mode 100644 index 0000000..d66203e --- /dev/null +++ b/app/composables/useIncrementalList.ts @@ -0,0 +1,93 @@ +import { computed, onBeforeUnmount, onMounted, ref, unref, watch, type ComputedRef, type Ref } from 'vue'; + +type ReactiveValue = Ref | ComputedRef; + +type UseIncrementalListOptions = { + pageSize?: number; + enabled?: ReactiveValue; + resetKeys?: Array | unknown>; +}; + +export function useIncrementalList( + items: ReactiveValue, + options: UseIncrementalListOptions = {}, +) { + const pageSize = options.pageSize ?? 24; + const enabled = computed(() => unref(options.enabled ?? true)); + const visibleCount = ref(pageSize); + const loadMoreSentinel = ref(null); + let observer: IntersectionObserver | null = null; + + const visibleItems = computed(() => ( + enabled.value + ? items.value.slice(0, visibleCount.value) + : items.value + )); + + const canLoadMore = computed(() => ( + enabled.value && visibleCount.value < items.value.length + )); + + const remainingCount = computed(() => Math.max(items.value.length - visibleCount.value, 0)); + + function loadMore() { + visibleCount.value = Math.min(visibleCount.value + pageSize, items.value.length); + } + + function resetVisibleCount() { + visibleCount.value = pageSize; + } + + function disconnectObserver() { + observer?.disconnect(); + observer = null; + } + + function connectObserver() { + disconnectObserver(); + + if (!canLoadMore.value || !loadMoreSentinel.value) { + return; + } + + observer = new IntersectionObserver((entries) => { + if (!entries.some((entry) => entry.isIntersecting)) { + return; + } + + loadMore(); + }, { + rootMargin: '240px 0px', + }); + + observer.observe(loadMoreSentinel.value); + } + + const resetSignature = computed(() => JSON.stringify([ + enabled.value, + items.value.length, + ...((options.resetKeys ?? []).map((entry) => unref(entry as ReactiveValue))), + ])); + + watch(resetSignature, resetVisibleCount, { immediate: true }); + watch(canLoadMore, () => { + if (!canLoadMore.value) { + disconnectObserver(); + } + }); + + onMounted(() => { + watch([loadMoreSentinel, canLoadMore], connectObserver, { immediate: true }); + }); + + onBeforeUnmount(disconnectObserver); + + return { + canLoadMore, + loadMore, + loadMoreSentinel, + remainingCount, + visibleCount, + visibleItems, + }; +} diff --git a/app/pages/bonus-system/index.vue b/app/pages/bonus-system/index.vue index 6d08055..b28ec9b 100644 --- a/app/pages/bonus-system/index.vue +++ b/app/pages/bonus-system/index.vue @@ -56,33 +56,40 @@ const referralLinksByReferrer = computed(() => { const filteredBalances = computed(() => { const query = search.value.trim().toLowerCase(); - return balances.value.filter((item) => { - const links = referralLinksByReferrer.value.get(item.userId); + return balances.value + .filter((item) => { + const links = referralLinksByReferrer.value.get(item.userId); - if (!links?.length) { - return false; - } + if (!links?.length) { + return false; + } - if (!query) { - return true; - } + if (!query) { + return true; + } - return [ - item.fullName, - item.email, - item.companyName || '', - String(item.balance), - ...links.flatMap((link) => [ - link.refereeName, - link.refereeEmail, - link.refereeCompanyName || '', - String(link.bonusPercent), - ]), - ] - .join(' ') - .toLowerCase() - .includes(query); - }); + return [ + item.fullName, + item.email, + item.companyName || '', + String(item.balance), + ...links.flatMap((link) => [ + link.refereeName, + link.refereeEmail, + link.refereeCompanyName || '', + String(link.bonusPercent), + ]), + ] + .join(' ') + .toLowerCase() + .includes(query); + }) + .slice() + .sort((left, right) => { + const leftLatest = referralLinksByReferrer.value.get(left.userId)?.[0]?.createdAt ?? ''; + const rightLatest = referralLinksByReferrer.value.get(right.userId)?.[0]?.createdAt ?? ''; + return rightLatest.localeCompare(leftLatest); + }); }); const filteredWithdrawals = computed(() => { @@ -107,6 +114,30 @@ const filteredWithdrawals = computed(() => { }); }); +const { + canLoadMore: canLoadMoreBalances, + loadMore: loadMoreBalances, + loadMoreSentinel: loadMoreBalancesSentinel, + remainingCount: remainingBalancesCount, + visibleItems: visibleBalances, +} = useIncrementalList(filteredBalances, { + pageSize: 24, + enabled: computed(() => activeTab.value === 'balances'), + resetKeys: [search, activeTab], +}); + +const { + canLoadMore: canLoadMoreWithdrawals, + loadMore: loadMoreWithdrawals, + loadMoreSentinel: loadMoreWithdrawalsSentinel, + remainingCount: remainingWithdrawalsCount, + visibleItems: visibleWithdrawals, +} = useIncrementalList(filteredWithdrawals, { + pageSize: 24, + enabled: computed(() => activeTab.value === 'withdrawals'), + resetKeys: [search, activeTab], +}); + function userInitials(fullName: string) { const parts = fullName .trim() @@ -150,17 +181,29 @@ function formatAmount(value: number) {
Бонусных связок пока нет.
-
- +
+
+ +
+ +
+ +
@@ -173,7 +216,7 @@ function formatAmount(value: number) {
@@ -190,6 +233,16 @@ function formatAmount(value: number) {
+ +
+ +
diff --git a/app/pages/bonus-system/referrals/new.vue b/app/pages/bonus-system/referrals/new.vue index 0a3812d..9d49a41 100644 --- a/app/pages/bonus-system/referrals/new.vue +++ b/app/pages/bonus-system/referrals/new.vue @@ -27,14 +27,32 @@ const createReferralMutation = useMutation(CreateReferralDocument); const clientOptions = computed(() => ( (usersQuery.result.value?.managerUsers ?? []) .filter((user) => user.role === 'CLIENT') - .slice() - .sort((left, right) => left.fullName.localeCompare(right.fullName, 'ru')) +)); + +const referrerOptions = computed(() => ( + clientOptions.value.filter((user) => user.id !== refereeUserId.value) +)); + +const refereeOptions = computed(() => ( + clientOptions.value.filter((user) => user.id !== referrerUserId.value) )); const referralLinks = computed(() => ( linksQuery.result.value?.managerReferralLinks ?? [] )); +watch(referrerUserId, (value) => { + if (value && value === refereeUserId.value) { + refereeUserId.value = ''; + } +}); + +watch(refereeUserId, (value) => { + if (value && value === referrerUserId.value) { + referrerUserId.value = ''; + } +}); + function userOptionLabel(user: ManagerUserItem) { return [user.fullName, user.companyName || user.email] .filter(Boolean) @@ -96,7 +114,7 @@ async function createReferral() { Клиент, который получает бонус @@ -106,7 +124,7 @@ async function createReferral() { Клиент, с чьих заказов начисляется бонус diff --git a/app/pages/client-orders/index.vue b/app/pages/client-orders/index.vue index ebe0087..f5c005e 100644 --- a/app/pages/client-orders/index.vue +++ b/app/pages/client-orders/index.vue @@ -143,6 +143,18 @@ const searchedOrders = computed(() => { const filteredOrders = computed(() => searchedOrders.value.filter((order) => matchesFilter(order))); +const { + canLoadMore, + loadMore, + loadMoreSentinel, + remainingCount, + visibleItems: visibleOrders, +} = useIncrementalList(filteredOrders, { + pageSize: 24, + enabled: computed(() => viewMode.value === 'list'), + resetKeys: [search, statusFilter, viewMode], +}); + function escapeHtml(value: string) { return String(value) .replaceAll('&', '&') @@ -315,7 +327,7 @@ const calendarOptions = computed(() => ({
({ :total-price="order.totalPrice" :items="order.items" /> + +
+ +
diff --git a/app/pages/clients/[id].vue b/app/pages/clients/[id].vue index 83687d3..8859d17 100644 --- a/app/pages/clients/[id].vue +++ b/app/pages/clients/[id].vue @@ -46,6 +46,18 @@ const currentRequest = computed(() => const currentUserOrders = computed(() => userOrdersQuery.result.value?.managerOrders ?? []); +const { + canLoadMore: canLoadMoreUserOrders, + loadMore: loadMoreUserOrders, + loadMoreSentinel: loadMoreUserOrdersSentinel, + remainingCount: remainingUserOrdersCount, + visibleItems: visibleUserOrders, +} = useIncrementalList(currentUserOrders, { + pageSize: 24, + enabled: computed(() => !isRequestMode.value), + resetKeys: [entityId, isRequestMode], +}); + function userInitials(fullName: string) { const parts = fullName .trim() @@ -213,7 +225,7 @@ async function rejectRequest() {
+ +
+ +
diff --git a/app/pages/clients/index.vue b/app/pages/clients/index.vue index d7317b7..c05ef2f 100644 --- a/app/pages/clients/index.vue +++ b/app/pages/clients/index.vue @@ -79,6 +79,30 @@ const filteredRequests = computed(() => { }); }); +const { + canLoadMore: canLoadMoreUsers, + loadMore: loadMoreUsers, + loadMoreSentinel: loadMoreUsersSentinel, + remainingCount: remainingUsersCount, + visibleItems: visibleUsers, +} = useIncrementalList(filteredUsers, { + pageSize: 24, + enabled: computed(() => activeTab.value === 'users'), + resetKeys: [search, activeTab], +}); + +const { + canLoadMore: canLoadMoreRequests, + loadMore: loadMoreRequests, + loadMoreSentinel: loadMoreRequestsSentinel, + remainingCount: remainingRequestsCount, + visibleItems: visibleRequests, +} = useIncrementalList(filteredRequests, { + pageSize: 24, + enabled: computed(() => activeTab.value === 'requests'), + resetKeys: [search, activeTab], +}); + function userInitials(fullName: string) { const parts = fullName .trim() @@ -132,15 +156,27 @@ function userInitials(fullName: string) {
Пользователи по текущему запросу не найдены.
-
- +
+
+ +
+ +
+ +
@@ -151,27 +187,39 @@ function userInitials(fullName: string) {
Заявки по текущему запросу не найдены.
-
- -
-
-

{{ request.companyName }}

-

{{ request.contactName }}

+
+
+ +
+
+

{{ request.companyName }}

+

{{ request.contactName }}

+
+ {{ requestStatusLabel(request.status) }}
- {{ requestStatusLabel(request.status) }} -
-
-

{{ request.email }}

-

ИНН: {{ request.inn }}

-

{{ new Date(request.createdAt).toLocaleDateString() }}

-
- +
+

{{ request.email }}

+

ИНН: {{ request.inn }}

+

{{ new Date(request.createdAt).toLocaleDateString() }}

+
+ +
+ +
+ +
diff --git a/app/pages/orders/index.vue b/app/pages/orders/index.vue index ec9de66..5e9befc 100644 --- a/app/pages/orders/index.vue +++ b/app/pages/orders/index.vue @@ -45,6 +45,17 @@ const filteredOrders = computed(() => { return matchSearch && matchesFilter(order); }); }); + +const { + canLoadMore, + loadMore, + loadMoreSentinel, + remainingCount, + visibleItems: visibleOrders, +} = useIncrementalList(filteredOrders, { + pageSize: 24, + resetKeys: [search, statusFilter], +});