Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Claiming of Redeemed Vouchers #24

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
20 changes: 19 additions & 1 deletion components/voucher/List.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<script setup lang="ts">
import DataTable from 'primevue/datatable'
import Card from 'primevue/card'
import Column from 'primevue/column'
import ConfirmDialog from 'primevue/confirmdialog'
import Button from 'primevue/button'
import Skeleton from 'primevue/skeleton'
import Toast from 'primevue/toast'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import InputNumber from 'primevue/inputnumber'
import Textarea from 'primevue/textarea'
import { useConfirm } from 'primevue/useconfirm'
import { useToast } from 'primevue/usetoast'
import type { DefaultVoucher } from '~/shared/types'

Expand All @@ -20,7 +23,9 @@ const formData = reactive({
})

const route = useRoute()
const router = useRouter()
const toast = useToast()
const confirm = useConfirm()

const { data: vouchers, isLoading: vouchersIsLoading, error: vouchersError } = useVouchers()
const { mutate: updateMutate } = useUpdateVoucher()
Expand Down Expand Up @@ -67,10 +72,23 @@ function deleteVoucher(voucherId: string) {
},
})
}

function confirmDeleteVoucher(VoucherData: any) {
confirm.require ({
message: `Are you sure you want to delete this ${VoucherData.name} voucher?`,
header: 'Delete Voucher Confirmation',
icon: 'i-tabler-alert-circle',
acceptClass: 'p-button-danger',
accept: () => {
deleteVoucher(VoucherData.id)
},
})
}
</script>

<template>
<div>
<ConfirmDialog />
<Toast />

<Dialog v-model:visible="formData.visible" modal header="Edit voucher" class="min-w-sm">
Expand Down Expand Up @@ -130,7 +148,7 @@ function deleteVoucher(voucherId: string) {
<template #body="slotProps">
<div>
<Button size="small" text label="Edit" @click="onEditVoucher(slotProps.data)" />
<Button size="small" text severity="danger" label="Delete" @click="deleteVoucher(slotProps.data.id)" />
<Button size="small" text severity="danger" label="Delete" @click="confirmDeleteVoucher(slotProps.data)" />
</div>
</template>
</Column>
Expand Down
87 changes: 87 additions & 0 deletions components/voucher/RedemptionList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup lang="ts">
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import ConfirmDialog from 'primevue/confirmdialog'
import Button from 'primevue/button'
import Card from 'primevue/card'
import Skeleton from 'primevue/skeleton'
import Toast from 'primevue/toast'
import { useConfirm } from 'primevue/useconfirm'
import { useToast } from 'primevue/usetoast'

const route = useRoute()
const router = useRouter()
const toast = useToast()
const confirm = useConfirm()

const { data: redemptions, isLoading: redemptionsIsLoading, error: redemptionsError } = useClaimRedeems()
const { mutate: updateMutate, isLoading: isUpdateLoading } = useAdminClaimRedeem()

function claimVoucher(redemptionId: any, userId: any) {
updateMutate({
redemptionId,
userId,
},
{
onSuccess() {
toast.add({
summary: 'Voucher Claimed!',
C4RR0T02 marked this conversation as resolved.
Show resolved Hide resolved
severity: 'success',
life: 3000,
})
},
})
}

function confirmClaimVoucher(redemptionData: any) {
confirm.require({
message: 'You should only redeem this voucher at the voucher redemption location.',
header: `Are you sure you want to redeem this ${redemptionData.voucher.name} voucher?`,
icon: 'i-tabler-alert-circle',
acceptClass: 'p-button-danger',
accept: () => {
claimVoucher(redemptionData.id, redemptionData.user.id)
},
})
}
</script>

<template>
<div>
<ConfirmDialog />
<Toast />

<Skeleton v-if="redemptionsIsLoading" height="300px" />
<LazyErrorCard v-else-if="redemptionsError" v-bind="redemptionsError" />

<template v-else>
<Card v-if="redemptions?.length === 0">
<template #title>
No vouchers for claiming
C4RR0T02 marked this conversation as resolved.
Show resolved Hide resolved
</template>
<template #subtitle>
No vouchers are currently unclaimed. Please check back later.
C4RR0T02 marked this conversation as resolved.
Show resolved Hide resolved
</template>
</Card>

<DataTable v-else :value="redemptions">
<Column field="voucher.name" header="Voucher Name" sortable />
<Column field="user.name" header="User Name" sortable />
<Column field="claimed" header="Unclaimed">
<template #body="bodySlot">
<Button
v-if="bodySlot.data.claimed === false"
v-model="bodySlot.data.claimed"
C4RR0T02 marked this conversation as resolved.
Show resolved Hide resolved
label="Claim Now"
C4RR0T02 marked this conversation as resolved.
Show resolved Hide resolved
severity="danger"
@click="confirmClaimVoucher(bodySlot.data)"
/>
<div v-else>
Claimed
</div>
</template>
</Column>
</DataTable>
</template>
</div>
</template>
37 changes: 36 additions & 1 deletion composables/voucher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { CreateVoucherInput, RedeemVoucherInput, UpdateVoucherInput } from '~/shared/voucher'
import type { CreateVoucherInput, RedeemAdminClaimInput, RedeemClaimInput, RedeemVoucherInput, UpdateVoucherInput } from '~/shared/voucher'

export function useVouchers() {
const { $client } = useNuxtApp()
Expand Down Expand Up @@ -67,3 +67,38 @@ export function useRedeemVoucher() {
},
})
}

export function useClaimRedeems() {
const { $client } = useNuxtApp()

return useQuery({
queryKey: ['vouchersRedeemed'],
queryFn: () => $client.redeem.listVoucherRequireRedeem.query(),
})
}

export function useClaimRedeem() {
const { $client } = useNuxtApp()
const queryClient = useQueryClient()

return useMutation({
mutationFn: (input: RedeemClaimInput) => $client.redeem.claim.mutate(input),
onSuccess() {
queryClient.invalidateQueries(['vouchersRedeemed'])
queryClient.invalidateQueries(['me'])
},
})
}

export function useAdminClaimRedeem() {
const { $client } = useNuxtApp()
const queryClient = useQueryClient()

return useMutation({
mutationFn: (input: RedeemAdminClaimInput) => $client.redeem.adminClaim.mutate(input),
onSuccess() {
queryClient.invalidateQueries(['vouchersRedeemed'])
queryClient.invalidateQueries(['me'])
},
})
}
5 changes: 5 additions & 0 deletions pages/dashboard/institution/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const items = computed(() => [
match: ['/dashboard/institution/settings'],
name: 'Vouchers',
},
{
to: '/dashboard/institution/settings/redemption',
match: ['/dashboard/institution/settings/redemption'],
name: 'Vouchers Redemptions',
},
{
to: `/dashboard/institution/settings/invites?institutionId=${me?.value?.institution?.id}`,
match: ['/dashboard/institution/settings/invites'],
Expand Down
27 changes: 27 additions & 0 deletions pages/dashboard/institution/settings/redemption.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import Divider from 'primevue/divider'

const Toast = defineAsyncComponent(() => import('primevue/toast'))
</script>

<template>
<div>
<Toast />

<!-- TODO there are like 3 different ways this same layout is implemented across the app, should probably standardize -->
<div flex items-center justify-between>
<div>
<h2 text-lg font-medium>
Vouchers Redeemed
</h2>
<p text-sm opacity-80>
List of all vouchers redemptions by users
</p>
</div>
</div>

<Divider :pt="{ root: { class: 'before:border-solid!' } }" />

<VoucherRedemptionList />
</div>
</template>
68 changes: 64 additions & 4 deletions pages/dashboard/settings.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import DataTable from 'primevue/datatable'
import Button from 'primevue/button'
import Card from 'primevue/card'
import Column from 'primevue/column'
import ConfirmDialog from 'primevue/confirmdialog'
import DataTable from 'primevue/datatable'
import ScrollPanel from 'primevue/scrollpanel'
import Toast from 'primevue/toast'
import { useConfirm } from 'primevue/useconfirm'
import { useToast } from 'primevue/usetoast'
import { formatRelative } from 'date-fns'

// TODO yes, proper loading, etc.
const confirm = useConfirm()
const toast = useToast()
const { data: redeemed, isLoading: redeemedIsLoading } = useRedeemedVouchers()
const { mutate: updateMutate, isLoading: isUpdateLoading } = useClaimRedeem()

const relativeRedeemed = computed(() => {
const now = new Date()
Expand All @@ -14,18 +23,69 @@ const relativeRedeemed = computed(() => {
timestamp: formatRelative(v.timestamp, now),
}))
})

function claimVoucher(redemptionId: any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there two?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I believe this are not the same function instead one is a to create a confirmation message before a user officially confirms to claim the voucher.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant one on the component and one on the page file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe one is claimAdminVoucher and the other is claimVoucher

updateMutate({
redemptionId,
},
{
onSuccess() {
toast.add({
summary: 'Voucher Claimed!',
severity: 'success',
life: 3000,
})
},
})
}

function confirmClaimVoucher(redemptionData: any) {
confirm.require({
message: 'You should only redeem this voucher at the voucher redemption location.',
header: `Are you sure you want to redeem this ${redemptionData.voucher.name} voucher?`,
icon: 'i-tabler-alert-circle',
acceptClass: 'p-button-danger',
accept: () => {
claimVoucher(redemptionData.id)
},
})
}
</script>

<template>
<div w-full flex flex-col>
<CommonHeader title="User settings" />
<ConfirmDialog />
<Toast />
<CommonHeader title="Vouchers Redeemed" />

<div h-full overflow-y-auto>
<ScrollPanel style="height: 100%">
<div p4>
<DataTable :value="relativeRedeemed">
<Card v-if="relativeRedeemed?.length === 0">
<template #title>
No vouchers have been redeemed
</template>
<template #subtitle>
Redeem some vouchers to see them here.
</template>
</Card>
<DataTable v-else :value="relativeRedeemed">
<Column field="voucher.name" header="Voucher name" />
<Column field="timestamp" header="Redemption time" />
<Column field="claimed" header="Claim Status" sortable>
<template #body="bodySlot">
<Button
v-if="bodySlot.data.claimed === false"
v-model="bodySlot.data.claimed"
label="Claim Now"
severity="danger"
@click="confirmClaimVoucher(bodySlot.data)"
/>
<div v-else>
Claimed
</div>
</template>
</Column>
</DataTable>
</div>
</ScrollPanel>
Expand Down
2 changes: 2 additions & 0 deletions server/trpc/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { deadlineRouter } from './deadline/deadline.router'
import { eventRouter } from './event/event.router'
import { quizRouter } from './quiz/quiz.router'
import { voucherRouter } from './voucher/voucher.router'
import { redeemRouter } from './redemption/redemption.router'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can merge with voucher router?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a need to merge with voucher router?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Belongs under voucher domain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redemption cannot exist without voucher


export const appRouter = router({
hello: publicProcedure
Expand Down Expand Up @@ -42,6 +43,7 @@ export const appRouter = router({
event: eventRouter,
quiz: quizRouter,
voucher: voucherRouter,
redeem: redeemRouter,
})

// export type definition of API
Expand Down
Loading