feat(gift-cards): admin usage report for gift codes

Add report sheet with summary stats, table columns, and API hook for codes usage reporting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-10 11:48:22 +03:30
co-authored by Cursor
parent a778aad7bc
commit 87c35c3101
4 changed files with 294 additions and 0 deletions
+20
View File
@@ -7,6 +7,26 @@ import type {Giftcard, GiftcardInput} from "@/model/giftCard";
export const getGiftCardTable = async (params: TableInput) => export const getGiftCardTable = async (params: TableInput) =>
await useAxios.post<Array<Giftcard>>('/admin/giftcard/table', params) await useAxios.post<Array<Giftcard>>('/admin/giftcard/table', params)
export interface GiftCardCodesUsageSummary {
total_codes: number
used_codes: number
unused_codes: number
active_codes: number
expired_codes: number
}
export type GiftCardCodesUsageReportResponse = {
rows: Record<string, unknown>[]
summary?: GiftCardCodesUsageSummary
query?: Record<string, unknown>
}
export const getGiftCardCodesUsageReport = async (params: TableInput) =>
await useAxios.post<GiftCardCodesUsageReportResponse>(
'/admin/giftcard/codes-usage-report',
params,
)
export const createGiftCard = async (obj: GiftcardInput) => export const createGiftCard = async (obj: GiftcardInput) =>
await useAxios.post<Giftcard>(`/admin/giftcard/generate`, obj) await useAxios.post<Giftcard>(`/admin/giftcard/generate`, obj)
@@ -0,0 +1,104 @@
<template>
<BasicActionSheet
v-model:visible="visibleProxy"
title="گزارش استفاده کدهای هدیه"
modal-width="92%"
drawer-height="92vh"
:show-button="true"
:show-cancel="true"
:show-ok="false"
cancel-text="بستن"
@cancel="close"
@close="close"
>
<p class="m-0 mb-3 text-13px leading-6 text-[var(--admin-text-secondary)]">
هر سطر یک کد است. ستون «تعداد استفاده» برای کدهای عمومی چندمصرف، تعداد مصرف کل پویش است.
</p>
<div
v-if="summary"
class="mb-3 grid grid-cols-2 gap-2 sm:grid-cols-5"
>
<div class="rounded-lg border border-solid border-[var(--admin-border)] p-2.5 text-center">
<div class="text-xs text-[var(--admin-text-secondary)]">کل کدها</div>
<div class="text-base font-700">{{ summary.total_codes }}</div>
</div>
<div class="rounded-lg border border-solid border-[var(--admin-border)] p-2.5 text-center">
<div class="text-xs text-[var(--admin-text-secondary)]">استفاده‌شده</div>
<div class="text-base font-700 text-emerald-700">{{ summary.used_codes }}</div>
</div>
<div class="rounded-lg border border-solid border-[var(--admin-border)] p-2.5 text-center">
<div class="text-xs text-[var(--admin-text-secondary)]">بدون مصرف</div>
<div class="text-base font-700">{{ summary.unused_codes }}</div>
</div>
<div class="rounded-lg border border-solid border-[var(--admin-border)] p-2.5 text-center">
<div class="text-xs text-[var(--admin-text-secondary)]">فعال</div>
<div class="text-base font-700">{{ summary.active_codes }}</div>
</div>
<div class="rounded-lg border border-solid border-[var(--admin-border)] p-2.5 text-center col-span-2 sm:col-span-1">
<div class="text-xs text-[var(--admin-text-secondary)]">منقضی</div>
<div class="text-base font-700 text-red-600">{{ summary.expired_codes }}</div>
</div>
</div>
<BaseTable
class="h-[min(58vh,520px)] min-h-0 min-w-0"
v-bind="tblProps"
@register="submit"
/>
</BasicActionSheet>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import { getGiftCardCodesUsageReport } from '@/api/giftCard'
import { getGiftCardUsageReportColumns } from '../hooks/usageReportColumns'
const props = defineProps<{
visible: boolean
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value),
})
const summary = ref<{
total_codes: number
used_codes: number
unused_codes: number
active_codes: number
expired_codes: number
} | null>(null)
const [submit, reload] = useBaseTable()
const tblProps = ref({
columns: getGiftCardUsageReportColumns(),
api: async (params: TableInput) => {
const res = await getGiftCardCodesUsageReport(params)
summary.value = res.summary ?? null
return res
},
})
function close() {
visibleProxy.value = false
}
watch(
() => props.visible,
(open) => {
if (open) {
void reload()
}
},
)
</script>
@@ -23,6 +23,15 @@
> >
افزودن کد تخفیف افزودن کد تخفیف
</BaseButton> </BaseButton>
<BaseButton
v-if="!customerId && (can('usageReport') || can('codes'))"
type="default"
size="small"
icon="vuesax-linear:chart-2"
@click="usageReportVisible = true"
>
گزارش استفاده کدها
</BaseButton>
</div> </div>
</template> </template>
<template #action="{ column, record }"> <template #action="{ column, record }">
@@ -69,6 +78,8 @@
/> />
</div> </div>
</BasicShowContext> </BasicShowContext>
<GiftCardCodesUsageReportSheet v-model:visible="usageReportVisible" />
</div> </div>
</template> </template>
@@ -84,6 +95,7 @@ import { isMobile } from '@core/utils/is'
import { isStoreApp } from '@/utils/config' import { isStoreApp } from '@/utils/config'
import { giftCardsPageIntro } from '@/utils/adminHelpCopy' import { giftCardsPageIntro } from '@/utils/adminHelpCopy'
import { usePageActionAccess } from '@/composables/usePageActionAccess' import { usePageActionAccess } from '@/composables/usePageActionAccess'
import GiftCardCodesUsageReportSheet from './GiftCardCodesUsageReportSheet.vue'
interface Prop { interface Prop {
customerId?: number customerId?: number
@@ -92,6 +104,7 @@ interface Prop {
const props = defineProps<Prop>() const props = defineProps<Prop>()
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'giftcardcodes']) const emits = defineEmits(['delete', 'edit', 'add', 'info', 'giftcardcodes'])
const { can } = usePageActionAccess() const { can } = usePageActionAccess()
const usageReportVisible = ref(false)
const [submit, reload] = useBaseTable() const [submit, reload] = useBaseTable()
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null) const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
@@ -0,0 +1,157 @@
import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { metalLabel } from '@/utils/adminMetalLabels'
export function getGiftCardUsageReportColumns(): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
key: 'row',
filterable: false,
sortable: false,
customRender: (record: { index: number }) => record.index + 1,
},
{
title: 'کد',
dataIndex: 'code',
key: 'code',
type: 'text',
},
{
title: 'نام پویش',
dataIndex: 'campaign_name',
key: 'campaign_name',
type: 'text',
},
{
title: 'تعداد استفاده',
dataIndex: 'usage_count',
key: 'usage_count',
type: 'Number',
filterable: false,
},
{
title: 'وضعیت کد',
dataIndex: 'sts',
key: 'sts',
type: 'Number',
customRender: ({ record }) => {
const sts = Number(record.sts)
if (sts === 1) {
return (
<div class="flex justify-center">
<CustomTag textColor="#ca8a04" bgColor="#fef3c7">
فعال
</CustomTag>
</div>
)
}
if (sts === 2) {
return (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
استفاده شده
</CustomTag>
</div>
)
}
return (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
منقضی / غیرفعال
</CustomTag>
</div>
)
},
},
{
title: 'نوع',
dataIndex: 'unit',
key: 'unit',
filterable: false,
customRender: ({ record }) => {
const unit = Number(record.unit)
if (unit === 3) {
return (
<div class="flex justify-center">
<CustomTag textColor="#1d4ed8" bgColor="#dbeafe">
تخفیف
</CustomTag>
</div>
)
}
if (unit === 2) {
return (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
{metalLabel.goldWalletShort()}
</CustomTag>
</div>
)
}
return (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ریالی
</CustomTag>
</div>
)
},
},
{
title: 'مقدار',
dataIndex: 'amount',
key: 'amount',
type: 'Number',
filterable: false,
},
{
title: 'مصرف‌کننده',
dataIndex: 'assigned_to',
key: 'assigned_to_name',
filterable: false,
customRender: ({ record }) => record.assigned_to?.name ?? '-',
},
{
title: 'موبایل مصرف‌کننده',
dataIndex: 'assigned_to',
key: 'assigned_to_mobile',
filterable: false,
customRender: ({ record }) => (
<span dir="ltr" class="inline-block">
{record.assigned_to?.mobile ?? '-'}
</span>
),
},
{
title: 'تاریخ استفاده',
dataIndex: 'used_at',
key: 'used_at',
filterable: false,
customRender: ({ record }) => (record.used_at ? toJalali(record.used_at) : '-'),
},
{
title: 'انقضا',
dataIndex: 'expiry_date',
key: 'expiry_date',
filterable: false,
customRender: ({ record }) => (record.expiry_date ? toJalali(record.expiry_date) : '-'),
},
{
title: 'کل مجاز پویش',
dataIndex: 'campaign_count',
key: 'campaign_count',
type: 'Number',
filterable: false,
},
{
title: 'مصرف‌شده پویش',
dataIndex: 'campaign_used_count',
key: 'campaign_used_count',
type: 'Number',
filterable: false,
},
]
}