Skip to content

Commit

Permalink
πŸ’„ Add payout detail page (#185)
Browse files Browse the repository at this point in the history
* πŸ’„ Add payout history list

* πŸ’„ Add payout detail page

* 🚨 Auto fix lint

* πŸ’¬ Update table column names

Co-authored-by: Ng Wing Tat, David <[email protected]>

* 🚸 Only fetch payout list if stripe connect is ready

* πŸ’„ Improve payout details UI

* 🎨 Remove unneeded auth guard in payout page

* πŸ› Handle usdc decimal

---------

Co-authored-by: Ng Wing Tat, David <[email protected]>
Co-authored-by: Ng Wing Tat, David <[email protected]>
  • Loading branch information
3 people authored Sep 26, 2024
1 parent d1af5ed commit b700cb2
Show file tree
Hide file tree
Showing 3 changed files with 318 additions and 9 deletions.
113 changes: 104 additions & 9 deletions pages/nft-book-store/user/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/>

<UAlert
v-if="!connectStatus?.isReady"
v-if="!isStripeConnectReady"
icon="i-heroicons-exclamation-circle"
color="orange"
variant="soft"
Expand Down Expand Up @@ -45,7 +45,7 @@

<UFormGroup v-if="userLikerInfo?.user" label="Your affiliation channel ID" size="xl">
<UInput placeholder="Affiliation ID" :value="channelId" disabled />
<template v-if="!connectStatus.isReady" #help>
<template v-if="!isStripeConnectReady" #help>
Please setup your stripe account below to participate in the book affiliation program.
</template>
<template v-else #help>
Expand Down Expand Up @@ -100,7 +100,7 @@
]"
:rows="[{
initiated: connectStatus?.hasAccount || false,
completed: connectStatus?.isReady || false
completed: isStripeConnectReady || false
}]"
:ui="{ th: { base: 'text-center' }, td: { base: 'text-center' } }"
>
Expand All @@ -123,7 +123,7 @@
@click="onLoginToStripe"
/>
<UButton
v-else-if="connectStatus?.isReady"
v-else-if="isStripeConnectReady"
label="Login to Stripe account"
size="lg"
@click="onLoginToStripe"
Expand All @@ -137,7 +137,7 @@
</template>
</UCard>

<template v-if="connectStatus?.isReady">
<template v-if="isStripeConnectReady">
<UCard
:ui="{
header: { base: 'flex justify-between items-center' },
Expand Down Expand Up @@ -271,6 +271,57 @@
</template>
</UTable>
</UCard>
<UCard
:ui="{
header: { base: 'flex justify-between items-center' },
body: { padding: '' },
footer: { base: 'text-center' },
}"
>
<template #header>
<h1 class="text-center font-bold font-mono">
Commission Payout History
</h1>

<UTooltip
text="Refresh Status"
:popper="{ placement: 'left' }"
>
<UButton
icon="i-heroicons-arrow-path"
variant="outline"
:disabled="isLoading"
@click="loadPayoutHistory"
/>
</UTooltip>
</template>

<UTable
:columns="[
{ key: 'createdTs', label: 'Created' },
{ key: 'amount', label: 'Payout Amount' },
{ key: 'status', label: 'Status' },
{ key: 'arrivalTs', label: 'Arrived' },
{ key: 'details', label: 'Payout Details' },
]"
:rows="payoutHistoryRows"
:ui="{ th: { base: 'text-center' }, td: { base: 'text-center' } }"
>
<template #details-data="{ row }">
<UButton
label="Details"
size="sm"
color="gray"
:to="{
name: 'nft-book-store-user-payouts-payoutId',
params: {
payoutId: row.id
}
}"
/>
</template>
</UTable>
</UCard>
</template>
</template>
</PageBody>
Expand All @@ -297,6 +348,7 @@ const error = ref('')
const isLoading = ref(false)
const connectStatus = ref<any>({})
const commissionHistory = ref<any>([])
const payoutHistory = ref<any>([])
const isEnableNotificationEmails = ref(true)
const channelId = computed(() => {
Expand Down Expand Up @@ -325,8 +377,11 @@ onMounted(async () => {
refreshStripeConnectStatus(),
userStore.lazyFetchBookUserProfile()
])
if (isStripeConnectReady.value) { await loadPayoutHistory() }
})
const isStripeConnectReady = computed(() => connectStatus.value?.isReady)
const commissionHistoryRows = computed(() => {
return commissionHistory.value.map((row: any) => {
let type = row.type
Expand All @@ -341,14 +396,34 @@ const commissionHistoryRows = computed(() => {
return {
...row,
type,
amount: row.amount / 100,
amountTotal: row.amountTotal / 100,
currency: row.currency || 'usd',
amount: formatNumberWithCurrency(row.amount, row.currency),
amountTotal: formatNumberWithCurrency(row.amountTotal, row.currency),
currency: formatCurrency(row.currency),
timestamp: new Date(row.timestamp).toLocaleString()
}
})
})
const payoutHistoryRows = computed(() => {
return payoutHistory.value.map((row: any) => {
const {
id,
amount,
currency,
status,
arrivalTs,
createdTs
} = row
return {
id,
amount: formatNumberWithCurrency(amount, currency),
status,
createdTs: new Date(createdTs * 1000).toLocaleString(),
arrivalTs: arrivalTs ? new Date(arrivalTs * 1000).toLocaleString() : ''
}
})
})
async function loadCommissionHistory () {
try {
isLoading.value = true
Expand Down Expand Up @@ -379,6 +454,26 @@ async function loadCommissionHistory () {
}
}
async function loadPayoutHistory () {
try {
isLoading.value = true
const { data, error: fetchError } = await useFetch(`${LIKE_CO_API}/likernft/book/user/payouts/list`, {
headers: {
authorization: `Bearer ${token.value}`
}
})
if (fetchError.value && fetchError.value?.statusCode !== 404) {
throw new Error(fetchError.value.toString())
}
payoutHistory.value = (data.value as any)?.payouts || []
} catch (e) {
console.error(e)
error.value = (e as Error).toString()
} finally {
isLoading.value = false
}
}
async function refreshUserLikerInfo () {
try {
await userStore.fetchUserLikerInfo({ nocache: true })
Expand All @@ -393,7 +488,7 @@ async function refreshStripeConnectStatus () {
try {
await loadStripeConnectStatus()
if (connectStatus.value?.hasAccount && !connectStatus.value?.isReady) {
if (connectStatus.value?.hasAccount && !isStripeConnectReady.value) {
const { data, error: fetchError } = await useFetch(`${LIKE_CO_API}/likernft/book/user/connect/refresh`,
{
method: 'POST',
Expand Down
195 changes: 195 additions & 0 deletions pages/nft-book-store/user/payouts/[payoutId].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<template>
<PageBody class="space-y-10 pb-10">
<div class="flex justify-between gap-4">
<h1 class="text-lg font-bold font-mono flex-wrap">
Payout Details - {{ payoutId }}
</h1>
</div>

<UProgress v-if="isLoading" animation="carousel">
<template #indicator>
Loading...
</template>
</UProgress>

<UCard
:ui="{ body: { padding: '' } }"
>
<template #header>
<h3 class="font-bold font-mono">
Payout Information
</h3>
</template>

<UTable
:columns="[
{ key: 'id', label: 'ID' },
{ key: 'createdTs', label: 'Created' },
{ key: 'currency', label: 'Currency' },
{ key: 'amount', label: 'Payout Amount' },
{ key: 'status', label: 'Status' },
{ key: 'arrivalTs', label: 'Arrived' },
]"
:rows="payoutDataRows"
:loading="isLoading"
:ui="{ th: { base: 'text-center' }, td: { base: 'text-center' } }"
/>
</UCard>

<UCard :ui="{ body: { padding: '' } }">
<template #header>
<h3 class="text-sm font-bold font-mono">
Payout Commission Item Details
</h3>
</template>
<UTable
:columns="[
{ key: 'commissionId', label: 'Commission ID' },
{ key: 'createdTs', label: 'Created' },
{ key: 'currency', label: 'Currency' },
{ key: 'amount', label: 'Commission Amount' },
{ key: 'description', label: 'Description' },
{ key: 'status', label: 'Status' },
{ key: 'metadata', label: 'Metadata' },
]"
:rows="payoutItemRows"
:loading="isLoading"
:ui="{ th: { base: 'text-center' }, td: { base: 'text-center' } }"
>
<template #metadata-data="{ row }">
<UButton
label="View"
color="gray"
@click="modalMetadata = row.metadata"
/>
</template>
</UTable>
</UCard>

<UModal v-model="isModalMetadataOpen">
<UCard
:ui="{
header: { base: 'flex items-center justify-between gap-2' },
body: { base: 'overflow-auto text-sm' }
}"
>
<template #header>
<h4 class="font-mono font-bold" v-text="'Metadata'" />

<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
square
padded
@click="isModalMetadataOpen = false"
/>
</template>

<pre v-text="modalMetadata" />
</UCard>
</UModal>
</PageBody>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useBookStoreApiStore } from '~/stores/book-store-api'
const { LIKE_CO_API } = useRuntimeConfig().public
const bookStoreApiStore = useBookStoreApiStore()
const { token } = storeToRefs(bookStoreApiStore)
const route = useRoute()
const payoutData = ref<any>({})
const payoutItemDetails = ref<any>([])
const error = ref('')
const isLoading = ref(false)
const payoutId = ref(route.params.payoutId as string)
const modalMetadata = ref('')
const isModalMetadataOpen = computed({
get: () => !!modalMetadata.value,
set: (value) => {
if (!value) {
modalMetadata.value = ''
}
}
})
watch(isLoading, (newIsLoading) => {
if (newIsLoading) { error.value = '' }
})
const payoutDataRows = computed(() => {
if (!payoutData.value) { return [] }
const {
id,
createdTs,
amount,
status,
currency,
arrivalTs
} = payoutData.value
return [
{
id,
createdTs: new Date(createdTs * 1000).toLocaleString(),
amount: formatNumberWithCurrency(amount, currency),
status,
currency: formatCurrency(currency),
arrivalTs: arrivalTs ? new Date(arrivalTs * 1000).toLocaleString() : ''
}
]
})
const payoutItemRows = computed(() => {
return payoutItemDetails.value.map((row: any) => {
const {
commissionId,
createdTs,
amount,
currency,
description,
status,
metadata
} = row
return {
commissionId,
createdTs: new Date(createdTs * 1000).toLocaleString(),
amount: formatNumberWithCurrency(amount, currency),
currency: formatCurrency(currency),
description,
status,
metadata: JSON.stringify(metadata, null, 2)
}
})
})
onMounted(async () => {
try {
isLoading.value = true
const { data, error: fetchError } = await useFetch(`${LIKE_CO_API}/likernft/book/user/payouts/${payoutId.value}`,
{
headers: {
authorization: `Bearer ${token.value}`
}
})
if (fetchError.value) {
if (fetchError.value.statusCode === 403) {
error.value = 'NOT_OWNER_OF_NFT_CLASS'
} else {
error.value = fetchError.value.toString()
}
} else {
payoutData.value = (data.value as any)
if (payoutData.value.items) {
payoutItemDetails.value = payoutData.value.items
}
}
} finally {
isLoading.value = false
}
})
</script>
Loading

0 comments on commit b700cb2

Please sign in to comment.