gift card export, new category, login theme fix

This commit is contained in:
2026-09-20 17:25:25 +03:30
parent 7ff90b05d0
commit 8004dd52b3
6 changed files with 359 additions and 13 deletions
@@ -11,7 +11,33 @@
@cancel="close"
@close="close"
>
<div v-if="isMobile" class="mb-3 text-base font-700">لیست کدهای کارت هدیه</div>
<div class="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<p class="m-0 text-13px text-[var(--admin-text-secondary)]">
{{ codes.length }} کد — دانلود به صورت اکسل یا PDF
</p>
</div>
<a-dropdown
:trigger="['click']"
placement="bottomLeft"
:overlay-style="{ zIndex: SELECT_DROPDOWN_Z_INDEX }"
>
<BaseButton
type="default"
size="small"
icon="vuesax-linear:document-download"
:disabled="!codes.length"
>
دانلود
</BaseButton>
<template #overlay>
<a-menu @click="handleDownload">
<a-menu-item key="excel">اکسل (CSV)</a-menu-item>
<a-menu-item key="pdf">PDF</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<a-table
class="hidden md:block"
@@ -63,19 +89,25 @@
</template>
<script setup lang="ts">
import type { Giftcardcode } from '@/model/giftCard'
import type { Giftcard, Giftcardcode } from '@/model/giftCard'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import CustomTag from '@/components/CustomTag.vue'
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { toJalali } from '@core/utils/useDataTime'
import GiftCardCodeMobileDetailSheet from './GiftCardCodeMobileDetailSheet.vue'
import GiftCardCodeMobileListItem from './GiftCardCodeMobileListItem.vue'
import {
downloadGiftCardCodesExcel,
downloadGiftCardCodesPdf,
type GiftCardCodesExportMeta
} from '../utils/exportGiftCardCodes'
import { SELECT_DROPDOWN_Z_INDEX } from '@/constants/overlayZIndex'
const props = defineProps<{
visible: boolean
codes: Giftcardcode[]
giftCard?: Giftcard | null
}>()
const emit = defineEmits<{
@@ -104,4 +136,24 @@ const columns = [
function close() {
visibleProxy.value = false
}
const exportMeta = computed<GiftCardCodesExportMeta>(() => ({
id: props.giftCard?.id,
unit: props.giftCard?.unit,
count: props.giftCard?.count,
customerName: (props.giftCard as Giftcard & { customer?: { name?: string } })?.customer?.name,
customerMobile: (props.giftCard as Giftcard & { customer?: { mobile?: string } })?.customer?.mobile,
createdAt: props.giftCard?.created_at
}))
function handleDownload({ key }: { key: string }) {
if (!props.codes.length) return
if (key === 'excel') {
downloadGiftCardCodesExcel(props.codes, exportMeta.value)
return
}
if (key === 'pdf') {
downloadGiftCardCodesPdf(props.codes, exportMeta.value)
}
}
</script>
+17 -5
View File
@@ -91,7 +91,11 @@
:name="infoParams.name"
:items="infoParams.items"
/>
<GiftCardCodesSheet v-model:visible="giftCardCodesVisible" :codes="selectedGiftCardCodes" />
<GiftCardCodesSheet
v-model:visible="giftCardCodesVisible"
:codes="selectedGiftCardCodes"
:gift-card="selectedGiftCard"
/>
</div>
</template>
@@ -178,6 +182,7 @@ const form = ref<GiftcardInput>({
let infoParams = reactive<Info>({})
const giftCardCodesVisible = ref<boolean>(false)
const selectedGiftCardCodes = ref<Giftcardcode[]>([])
const selectedGiftCard = ref<Giftcard | null>(null)
function info(val: Giftcard) {
// نمایش اطلاعات اصلی در BasicInfoModal
@@ -209,8 +214,10 @@ function info(val: Giftcard) {
value: usePrice(val.total ?? 0)
},
{
key: 'کد یکتا',
value: val.unique_code ?? '-'
key: val.giftcardcodes?.length > 1 ? 'کدهای یکتا' : 'کد یکتا',
value: val.giftcardcodes?.length
? val.giftcardcodes.map((c) => c.code).join('، ')
: (val.unique_code ?? '-')
},
{
key: 'تعداد استفاده شده',
@@ -239,6 +246,7 @@ function info(val: Giftcard) {
function showGiftCardCodes(val: Giftcard) {
// نمایش کدهای کارت هدیه در مودال جداگانه
if (val.giftcardcodes && val.giftcardcodes.length > 0) {
selectedGiftCard.value = val
selectedGiftCardCodes.value = val.giftcardcodes
giftCardCodesVisible.value = true
} else {
@@ -341,11 +349,15 @@ const userInfoVisible = ref<boolean>(false)
//submit gift card
async function ok() {
if (loading.value) return
await giftCardEditorRef.value?.validateFields?.().then(async () => {
loading.value = true
try {
// تنظیم تاریخ انقضا به آخر همان روز (23:59:59) و تبدیل به میلادی
const formData = { ...form.value }
const formData = {
...form.value,
count: Number(form.value.count),
amount: Number(form.value.amount),
}
if (formData.expiry_date) {
// تبدیل به میلادی و تنظیم به آخر روز
formData.expiry_date = dayjs(formData.expiry_date)
@@ -0,0 +1,189 @@
import type { Giftcardcode } from '@/model/giftCard'
import { metalLabel } from '@/utils/adminMetalLabels'
import { toJalali } from '@core/utils/useDataTime'
export interface GiftCardCodesExportMeta {
id?: number
unit?: number
count?: number
customerName?: string
customerMobile?: string
createdAt?: string
}
interface ExportRow {
index: number
code: string
unit: string
amount: string
status: string
expiryDate: string
usedAt: string
assignedName: string
assignedMobile: string
}
const CSV_HEADERS = [
'ردیف',
'کد',
'نوع',
'مقدار',
'وضعیت',
'تاریخ انقضا',
'تاریخ استفاده',
'نام کاربر',
'موبایل کاربر'
]
function statusLabel(sts: number): string {
if (sts === 1) return 'فعال'
if (sts === 2) return 'استفاده شده'
return 'غیرفعال'
}
function unitLabel(unit: number): string {
if (unit === 2) return metalLabel.goldWalletShort()
if (unit === 3) return 'تخفیف'
return 'ریالی'
}
function amountLabel(code: Giftcardcode): string {
const suffix = code.unit === 2 ? 'گرم' : 'ریال'
return `${code.amount ?? '-'} ${suffix}`
}
function buildRows(codes: Giftcardcode[]): ExportRow[] {
return codes.map((code, index) => ({
index: index + 1,
code: code.code,
unit: unitLabel(code.unit),
amount: amountLabel(code),
status: statusLabel(code.sts),
expiryDate: code.expiry_date ? toJalali(code.expiry_date) : '-',
usedAt: code.used_at ? toJalali(code.used_at) : '-',
assignedName: code.assigned_to?.name ?? '-',
assignedMobile: code.assigned_to?.mobile ?? '-'
}))
}
function csvCell(value: unknown): string {
const text = value == null || value === '' ? '-' : String(value)
if (/[",\n\r]/.test(text)) {
return `"${text.replace(/"/g, '""')}"`
}
return text
}
function buildFilename(meta: GiftCardCodesExportMeta, extension: string): string {
const idPart = meta.id ? `-${meta.id}` : ''
const datePart = new Date().toISOString().slice(0, 10)
return `giftcard-codes${idPart}-${datePart}.${extension}`
}
function downloadBlob(content: string, filename: string, mime: string) {
const blob = new Blob([content], { type: mime })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
function buildSummary(meta: GiftCardCodesExportMeta): string[] {
const lines: string[] = []
if (meta.id) lines.push(`شناسه کارت هدیه: ${meta.id}`)
if (meta.unit != null) lines.push(`نوع: ${unitLabel(meta.unit)}`)
if (meta.count != null) lines.push(`تعداد کدها: ${meta.count}`)
if (meta.customerName) lines.push(`مشتری: ${meta.customerName}`)
if (meta.customerMobile) lines.push(`موبایل: ${meta.customerMobile}`)
if (meta.createdAt) lines.push(`تاریخ ثبت: ${toJalali(meta.createdAt)}`)
return lines
}
export function downloadGiftCardCodesExcel(codes: Giftcardcode[], meta: GiftCardCodesExportMeta = {}) {
const rows = buildRows(codes)
const lines = [
CSV_HEADERS.map(csvCell).join(','),
...rows.map((row) =>
[
row.index,
row.code,
row.unit,
row.amount,
row.status,
row.expiryDate,
row.usedAt,
row.assignedName,
row.assignedMobile
]
.map(csvCell)
.join(',')
)
]
const content = `\uFEFF${lines.join('\r\n')}`
downloadBlob(content, buildFilename(meta, 'csv'), 'text/csv;charset=utf-8')
}
export function downloadGiftCardCodesPdf(codes: Giftcardcode[], meta: GiftCardCodesExportMeta = {}) {
const rows = buildRows(codes)
const summary = buildSummary(meta)
const title = meta.id ? `لیست کدهای کارت هدیه #${meta.id}` : 'لیست کدهای کارت هدیه'
const tableRows = rows
.map(
(row) => `
<tr>
<td>${row.index}</td>
<td dir="ltr">${row.code}</td>
<td>${row.unit}</td>
<td>${row.amount}</td>
<td>${row.status}</td>
<td>${row.expiryDate}</td>
<td>${row.usedAt}</td>
<td>${row.assignedName}</td>
<td dir="ltr">${row.assignedMobile}</td>
</tr>`
)
.join('')
const html = `<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<title>${title}</title>
<style>
body { font-family: Tahoma, Arial, sans-serif; margin: 24px; color: #111; }
h1 { font-size: 18px; margin: 0 0 8px; }
.summary { font-size: 12px; color: #555; margin-bottom: 16px; line-height: 1.8; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: right; }
th { background: #f5f5f5; }
</style>
</head>
<body>
<h1>${title}</h1>
${summary.length ? `<div class="summary">${summary.map((line) => `<div>${line}</div>`).join('')}</div>` : ''}
<table>
<thead>
<tr>
${CSV_HEADERS.map((header) => `<th>${header}</th>`).join('')}
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
</body>
</html>`
const printWindow = window.open('', '_blank')
if (!printWindow) return
printWindow.document.open()
printWindow.document.write(html)
printWindow.document.close()
printWindow.focus()
window.setTimeout(() => {
printWindow.print()
printWindow.close()
}, 300)
}