web/satellite: add pagination to billing history
This change adds pagination to the billing history table. It uses the new invoice-history endpoint since we only list invoices in this table. Issue: https://github.com/storj/storj/issues/5479 Change-Id: I192d58503434203808a23a7c18e8d1feb6afc73f
This commit is contained in:
parent
614d213432
commit
6e3da022e0
@ -15,7 +15,7 @@ import {
|
||||
TokenAmount,
|
||||
NativePaymentHistoryItem,
|
||||
Wallet,
|
||||
PaymentWithConfirmations,
|
||||
PaymentWithConfirmations, PaymentHistoryParam, PaymentHistoryPage,
|
||||
} from '@/types/payments';
|
||||
import { HttpClient } from '@/utils/httpClient';
|
||||
import { Time } from '@/utils/time';
|
||||
@ -215,8 +215,13 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
* @returns list of payments history items
|
||||
* @throws Error
|
||||
*/
|
||||
public async paymentsHistory(): Promise<PaymentsHistoryItem[]> {
|
||||
const path = `${this.ROOT_PATH}/billing-history`;
|
||||
public async paymentsHistory(param: PaymentHistoryParam): Promise<PaymentHistoryPage> {
|
||||
let path = `${this.ROOT_PATH}/invoice-history?limit=${param.limit}`;
|
||||
if (param.startingAfter) {
|
||||
path = `${path}&starting_after=${param.startingAfter}`;
|
||||
} else if (param.endingBefore) {
|
||||
path = `${path}&ending_before=${param.endingBefore}`;
|
||||
}
|
||||
const response = await this.client.get(path);
|
||||
|
||||
if (!response.ok) {
|
||||
@ -227,9 +232,10 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
});
|
||||
}
|
||||
|
||||
const paymentsHistoryItems = await response.json();
|
||||
if (paymentsHistoryItems) {
|
||||
return paymentsHistoryItems.map(item =>
|
||||
const pageJson = await response.json();
|
||||
let items: PaymentsHistoryItem[] = [];
|
||||
if (pageJson.items) {
|
||||
items = pageJson.items.map(item =>
|
||||
new PaymentsHistoryItem(
|
||||
item.id,
|
||||
item.description,
|
||||
@ -245,7 +251,11 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
return new PaymentHistoryPage(
|
||||
items,
|
||||
pageJson.next,
|
||||
pageJson.previous,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,14 @@
|
||||
Billing History
|
||||
</h1>
|
||||
|
||||
<v-table :total-items-count="historyItems.length" class="billing-history__table">
|
||||
<v-table
|
||||
simple-pagination
|
||||
:total-items-count="historyItems.length"
|
||||
class="billing-history__table"
|
||||
:on-next-clicked="nextClicked"
|
||||
:on-previous-clicked="previousClicked"
|
||||
:on-page-size-changed="sizeChanged"
|
||||
>
|
||||
<template #head>
|
||||
<BillingHistoryHeader />
|
||||
</template>
|
||||
@ -23,16 +30,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
PaymentsHistoryItem,
|
||||
PaymentsHistoryItemStatus,
|
||||
PaymentsHistoryItemType,
|
||||
PaymentHistoryPage,
|
||||
} from '@/types/payments';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { useBillingStore } from '@/store/modules/billingStore';
|
||||
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
|
||||
|
||||
import BillingHistoryHeader
|
||||
from '@/components/account/billing/billingTabs/BillingHistoryHeader.vue';
|
||||
@ -43,20 +51,49 @@ import VTable from '@/components/common/VTable.vue';
|
||||
const billingStore = useBillingStore();
|
||||
const notify = useNotify();
|
||||
|
||||
async function fetchHistory(): Promise<void> {
|
||||
const limit = ref(DEFAULT_PAGE_LIMIT);
|
||||
|
||||
const historyPage = computed((): PaymentHistoryPage => {
|
||||
return billingStore.state.paymentsHistory;
|
||||
});
|
||||
|
||||
const historyItems = computed((): PaymentsHistoryItem[] => {
|
||||
return historyPage.value.items.filter((item: PaymentsHistoryItem) => {
|
||||
return item.status !== PaymentsHistoryItemStatus.Draft;
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchHistory(endingBefore = '', startingAfter = ''): Promise<void> {
|
||||
try {
|
||||
await billingStore.getPaymentsHistory();
|
||||
await billingStore.getPaymentsHistory({
|
||||
limit: limit.value,
|
||||
startingAfter,
|
||||
endingBefore,
|
||||
});
|
||||
} catch (error) {
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.BILLING_HISTORY_TAB);
|
||||
}
|
||||
}
|
||||
|
||||
const historyItems = computed((): PaymentsHistoryItem[] => {
|
||||
return billingStore.state.paymentsHistory.filter((item: PaymentsHistoryItem) => {
|
||||
return item.status !== PaymentsHistoryItemStatus.Draft && item.status !== PaymentsHistoryItemStatus.Empty
|
||||
&& (item.type === PaymentsHistoryItemType.Invoice || item.type === PaymentsHistoryItemType.Charge);
|
||||
});
|
||||
});
|
||||
async function sizeChanged(size: number) {
|
||||
limit.value = size;
|
||||
await fetchHistory();
|
||||
}
|
||||
|
||||
async function nextClicked(): Promise<void> {
|
||||
const length = historyPage.value.items.length;
|
||||
if (!historyPage.value.hasNext || !length) {
|
||||
return;
|
||||
}
|
||||
await fetchHistory('', historyPage.value.items[length - 1].id);
|
||||
}
|
||||
|
||||
async function previousClicked(): Promise<void> {
|
||||
if (!historyPage.value.hasPrevious || !historyPage.value.items.length) {
|
||||
return;
|
||||
}
|
||||
await fetchHistory(historyPage.value.items[0].id, '');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchHistory();
|
||||
|
@ -6,7 +6,26 @@
|
||||
<span v-if="totalItemsCount > 0" class="pagination-container__label">{{ totalItemsCount }} {{ itemsLabel }}</span>
|
||||
<span v-else class="pagination-container__label">No {{ itemsLabel }}</span>
|
||||
|
||||
<div v-if="totalPageCount > 1" class="pagination-container__pages">
|
||||
<div v-if="simplePagination" class="pagination-container__pages">
|
||||
<span
|
||||
tabindex="0"
|
||||
class="pagination-container__pages__button"
|
||||
@click="prevPage"
|
||||
@keyup.enter="prevPage"
|
||||
>
|
||||
<PaginationRightIcon class="pagination-container__pages__button__image reversed" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
tabindex="0"
|
||||
class="pagination-container__pages__button"
|
||||
@click="nextPage"
|
||||
@keyup.enter="nextPage"
|
||||
>
|
||||
<PaginationRightIcon class="pagination-container__pages__button__image" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="totalPageCount > 1" class="pagination-container__pages">
|
||||
<template v-for="page of pageItems">
|
||||
<span
|
||||
v-if="page.type === 'prev'"
|
||||
@ -58,7 +77,8 @@
|
||||
<div v-else class="pagination-container__pages-placeholder" />
|
||||
|
||||
<table-size-changer
|
||||
v-if="limit && totalPageCount && totalItemsCount > 10"
|
||||
v-if="(limit && totalPageCount && totalItemsCount > 10) || simplePagination"
|
||||
simple-pagination
|
||||
:item-count="totalItemsCount"
|
||||
:selected="pageSize"
|
||||
@change="sizeChanged"
|
||||
@ -89,13 +109,21 @@ const props = withDefaults(defineProps<{
|
||||
totalPageCount?: number;
|
||||
limit?: number;
|
||||
totalItemsCount?: number;
|
||||
simplePagination?: boolean;
|
||||
onPageChange?: PageChangeCallback | null;
|
||||
onNextClicked?: (() => Promise<void>) | null;
|
||||
onPreviousClicked?: (() => Promise<void>) | null;
|
||||
onPageSizeChanged?: ((size: number) => Promise<void>) | null;
|
||||
}>(), {
|
||||
itemsLabel: 'items',
|
||||
totalPageCount: 0,
|
||||
limit: 0,
|
||||
totalItemsCount: 0,
|
||||
simplePagination: false,
|
||||
onPageChange: null,
|
||||
onNextClicked: null,
|
||||
onPreviousClicked: null,
|
||||
onPageSizeChanged: null,
|
||||
});
|
||||
|
||||
const currentPageNumber = ref<number>(1);
|
||||
@ -160,11 +188,18 @@ const isLastPage = computed((): boolean => {
|
||||
});
|
||||
|
||||
function sizeChanged(size: number) {
|
||||
// if the new size is large enough to cause the page index to be out of range
|
||||
// we calculate an appropriate new page index.
|
||||
const maxPage = Math.ceil(Math.ceil(props.totalItemsCount / size));
|
||||
const page = currentPageNumber.value > maxPage ? maxPage : currentPageNumber.value;
|
||||
withLoading(async () => {
|
||||
if (props.simplePagination) {
|
||||
if (!props.onPageSizeChanged) {
|
||||
return;
|
||||
}
|
||||
await props.onPageSizeChanged(size);
|
||||
pageSize.value = size;
|
||||
}
|
||||
// if the new size is large enough to cause the page index to be out of range
|
||||
// we calculate an appropriate new page index.
|
||||
const maxPage = Math.ceil(Math.ceil(props.totalItemsCount / size));
|
||||
const page = currentPageNumber.value > maxPage ? maxPage : currentPageNumber.value;
|
||||
if (!props.onPageChange) {
|
||||
return;
|
||||
}
|
||||
@ -197,6 +232,10 @@ async function goToPage(index: number) {
|
||||
*/
|
||||
async function nextPage(): Promise<void> {
|
||||
await withLoading(async () => {
|
||||
if (props.simplePagination && props.onNextClicked) {
|
||||
await props.onNextClicked();
|
||||
return;
|
||||
}
|
||||
if (isLastPage.value || !props.onPageChange) {
|
||||
return;
|
||||
}
|
||||
@ -210,6 +249,10 @@ async function nextPage(): Promise<void> {
|
||||
*/
|
||||
async function prevPage(): Promise<void> {
|
||||
await withLoading(async () => {
|
||||
if (props.simplePagination && props.onPreviousClicked) {
|
||||
await props.onPreviousClicked();
|
||||
return;
|
||||
}
|
||||
if (isFirstPage.value || !props.onPageChange) {
|
||||
return;
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ const appStore = useAppStore();
|
||||
const props = defineProps<{
|
||||
selected: number | null;
|
||||
itemCount: number;
|
||||
simplePagination?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -53,7 +54,7 @@ const options = computed((): {label:string, value:number}[] => {
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '100', value: 100 },
|
||||
];
|
||||
if (props.itemCount < 1000) {
|
||||
if (props.itemCount < 1000 && !props.simplePagination) {
|
||||
return [{ label: 'All', value: props.itemCount }, ...opts];
|
||||
}
|
||||
return opts;
|
||||
|
@ -19,11 +19,15 @@
|
||||
<div class="table-footer">
|
||||
<table-pagination
|
||||
class="table-footer__pagination"
|
||||
:simple-pagination="simplePagination"
|
||||
:total-page-count="totalPageCount"
|
||||
:total-items-count="totalItemsCount"
|
||||
:items-label="itemsLabel"
|
||||
:limit="limit"
|
||||
:on-page-size-changed="onPageSizeChanged"
|
||||
:on-page-change="onPageChange"
|
||||
:on-next-clicked="onNextClicked"
|
||||
:on-previous-clicked="onPreviousClicked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,19 +44,27 @@ const props = withDefaults(defineProps<{
|
||||
limit?: number,
|
||||
totalItemsCount?: number,
|
||||
onPageChange?: PageChangeCallback | null;
|
||||
onNextClicked?: (() => Promise<void>) | null;
|
||||
onPreviousClicked?: (() => Promise<void>) | null;
|
||||
onPageSizeChanged?: ((size: number) => Promise<void>) | null;
|
||||
totalPageCount?: number,
|
||||
selectable?: boolean,
|
||||
selected?: boolean,
|
||||
showSelect?: boolean,
|
||||
simplePagination?: boolean,
|
||||
}>(), {
|
||||
selectable: false,
|
||||
selected: false,
|
||||
showSelect: false,
|
||||
simplePagination: false,
|
||||
totalPageCount: 0,
|
||||
itemsLabel: 'items',
|
||||
limit: 0,
|
||||
totalItemsCount: 0,
|
||||
onPageChange: null,
|
||||
onNextClicked: null,
|
||||
onPreviousClicked: null,
|
||||
onPageSizeChanged: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selectAllClicked']);
|
||||
|
@ -10,10 +10,9 @@ import {
|
||||
CreditCard,
|
||||
DateRange,
|
||||
NativePaymentHistoryItem,
|
||||
PaymentHistoryPage,
|
||||
PaymentHistoryParam,
|
||||
PaymentsApi,
|
||||
PaymentsHistoryItem,
|
||||
PaymentsHistoryItemStatus,
|
||||
PaymentsHistoryItemType,
|
||||
PaymentStatus,
|
||||
PaymentWithConfirmations,
|
||||
ProjectCharges,
|
||||
@ -25,7 +24,7 @@ import { PaymentsHttpApi } from '@/api/payments';
|
||||
export class PaymentsState {
|
||||
public balance: AccountBalance = new AccountBalance();
|
||||
public creditCards: CreditCard[] = [];
|
||||
public paymentsHistory: PaymentsHistoryItem[] = [];
|
||||
public paymentsHistory: PaymentHistoryPage = new PaymentHistoryPage([]);
|
||||
public pendingPaymentsWithConfirmations: PaymentWithConfirmations[] = [];
|
||||
public nativePaymentsHistory: NativePaymentHistoryItem[] = [];
|
||||
public projectCharges: ProjectCharges = new ProjectCharges();
|
||||
@ -121,8 +120,8 @@ export const useBillingStore = defineStore('billing', () => {
|
||||
state.creditCards = state.creditCards.filter(card => card.id !== cardId);
|
||||
}
|
||||
|
||||
async function getPaymentsHistory(): Promise<void> {
|
||||
state.paymentsHistory = await api.paymentsHistory();
|
||||
async function getPaymentsHistory(params: PaymentHistoryParam): Promise<void> {
|
||||
state.paymentsHistory = await api.paymentsHistory(params);
|
||||
}
|
||||
|
||||
async function getNativePaymentsHistory(): Promise<void> {
|
||||
@ -191,7 +190,7 @@ export const useBillingStore = defineStore('billing', () => {
|
||||
function clear(): void {
|
||||
state.balance = new AccountBalance();
|
||||
state.creditCards = [];
|
||||
state.paymentsHistory = [];
|
||||
state.paymentsHistory = new PaymentHistoryPage([]);
|
||||
state.nativePaymentsHistory = [];
|
||||
state.projectCharges = new ProjectCharges();
|
||||
state.usagePriceModel = new ProjectUsagePriceModel();
|
||||
@ -206,18 +205,6 @@ export const useBillingStore = defineStore('billing', () => {
|
||||
return state.balance.sum > 0 || state.creditCards.length > 0;
|
||||
});
|
||||
|
||||
const isTransactionProcessing = computed((): boolean => {
|
||||
return state.balance.sum === 0 &&
|
||||
state.paymentsHistory.some((item: PaymentsHistoryItem) => {
|
||||
return item.amount >= 50 && item.type === PaymentsHistoryItemType.Transaction &&
|
||||
(
|
||||
item.status === PaymentsHistoryItemStatus.Pending ||
|
||||
item.status === PaymentsHistoryItemStatus.Paid ||
|
||||
item.status === PaymentsHistoryItemStatus.Completed
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const isBalancePositive = computed((): boolean => {
|
||||
return state.balance.sum > 0;
|
||||
});
|
||||
@ -225,7 +212,6 @@ export const useBillingStore = defineStore('billing', () => {
|
||||
return {
|
||||
state,
|
||||
canUserCreateFirstProject,
|
||||
isTransactionProcessing,
|
||||
isBalancePositive,
|
||||
getBalance,
|
||||
getWallet,
|
||||
|
@ -4,6 +4,15 @@
|
||||
import { formatPrice, decimalShift } from '@/utils/strings';
|
||||
import { JSONRepresentable } from '@/types/json';
|
||||
|
||||
/**
|
||||
* Page parameters for listing payments history.
|
||||
*/
|
||||
export interface PaymentHistoryParam {
|
||||
limit: number;
|
||||
startingAfter: string;
|
||||
endingBefore: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes all payments-related functionality
|
||||
*/
|
||||
@ -68,7 +77,7 @@ export interface PaymentsApi {
|
||||
* @returns list of payments history items
|
||||
* @throws Error
|
||||
*/
|
||||
paymentsHistory(): Promise<PaymentsHistoryItem[]>;
|
||||
paymentsHistory(param: PaymentHistoryParam): Promise<PaymentHistoryPage>;
|
||||
|
||||
/**
|
||||
* Returns a list of invoices, transactions and all others payments history items for payment account.
|
||||
@ -186,6 +195,18 @@ export class PaymentAmountOption {
|
||||
) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* PaymentHistoryPage holds a paged list of PaymentsHistoryItem.
|
||||
*/
|
||||
export class PaymentHistoryPage {
|
||||
public constructor(
|
||||
public readonly items: PaymentsHistoryItem[],
|
||||
public readonly hasNext = false,
|
||||
public readonly hasPrevious = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PaymentsHistoryItem holds all public information about payments history line.
|
||||
*/
|
||||
|
@ -6,12 +6,11 @@ import {
|
||||
Coupon,
|
||||
CreditCard,
|
||||
PaymentsApi,
|
||||
PaymentsHistoryItem,
|
||||
ProjectUsagePriceModel,
|
||||
TokenDeposit,
|
||||
NativePaymentHistoryItem,
|
||||
Wallet,
|
||||
ProjectCharges,
|
||||
ProjectCharges, PaymentHistoryParam, PaymentHistoryPage,
|
||||
} from '@/types/payments';
|
||||
|
||||
/**
|
||||
@ -56,8 +55,8 @@ export class PaymentsMock implements PaymentsApi {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
paymentsHistory(): Promise<PaymentsHistoryItem[]> {
|
||||
return Promise.resolve([]);
|
||||
paymentsHistory(param: PaymentHistoryParam): Promise<PaymentHistoryPage> {
|
||||
return Promise.resolve(new PaymentHistoryPage([]));
|
||||
}
|
||||
|
||||
nativePaymentsHistory(): Promise<NativePaymentHistoryItem[]> {
|
||||
|
Loading…
Reference in New Issue
Block a user