import { useState } from 'react'; import { Button, Descriptions, Drawer, Form, Input, InputNumber, Modal, Popconfirm, Select, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { AdminBenefitGrantRequest } from '@dukang/shared-types'; import { request } from '../lib/api'; import { COUPON_STATUS_LABELS, fmtTime } from '../lib/constants'; import RedeemRecordDetailDescriptions from '../components/RedeemRecordDetailDescriptions'; import { useAdminList } from '../lib/useAdminList'; type Row = { id: string; couponNo: string; totalAmount: number; balance: number; usedAmount: number; status: string; sourceProduct: string; createdAt: string; user?: { userNo: string; phone: string | null }; order?: { orderNo: string } | null; }; type CouponRedeemRecord = { id: string; redeemNo: string; amount: number; settleAmount: number; couponAmount?: number; role?: 'PRIMARY' | 'SECONDARY'; createdAt: string; store?: { id: string; name: string; cityName?: string | null } | null; }; type CouponRedeemSummary = { couponNo: string; totalAmount: number; usedAmount: number; balance: number; status: string; redeemCount: number; redeemRecordSum: number; }; type CouponDetail = Row & { redeemSummary?: CouponRedeemSummary | null; redeemRecords?: CouponRedeemRecord[]; user?: { userNo?: string; phone?: string | null }; order?: { orderNo?: string } | null; }; export default function BenefitCouponsPage() { const [form] = Form.useForm(); const [grantForm] = Form.useForm(); const [filters, setFilters] = useState>({}); const [grantOpen, setGrantOpen] = useState(false); const [granting, setGranting] = useState(false); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/benefit/coupons', () => { const qs = new URLSearchParams(); if (filters.couponNo) qs.set('couponNo', filters.couponNo); if (filters.status) qs.set('status', filters.status); if (filters.userId) qs.set('userId', filters.userId); return qs; }, [filters], ); const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [redeemDetail, setRedeemDetail] = useState | null>(null); const [redeemDrawerOpen, setRedeemDrawerOpen] = useState(false); const [redeemDetailLoading, setRedeemDetailLoading] = useState(false); async function openRedeemDetail(redeemId: string) { setRedeemDetailLoading(true); setRedeemDrawerOpen(true); try { const res = await request>(`/admin/redeem-records/${redeemId}`); setRedeemDetail(res); } catch (e) { message.error(e instanceof Error ? e.message : '加载核销详情失败'); setRedeemDrawerOpen(false); } finally { setRedeemDetailLoading(false); } } const columns: ColumnsType = [ { title: '券号', dataIndex: 'couponNo', width: 200, ellipsis: false }, { title: '用户', dataIndex: ['user', 'userNo'], width: 120, ellipsis: false }, { title: '手机号', dataIndex: ['user', 'phone'], width: 120, render: (v) => v || '—' }, { title: '订单', dataIndex: ['order', 'orderNo'], width: 180, ellipsis: false, render: (v) => v || '—', }, { title: '总额', dataIndex: 'totalAmount', width: 80, render: (v) => `¥${v}` }, { title: '余额', dataIndex: 'balance', width: 80, render: (v) => `¥${v}` }, { title: '状态', dataIndex: 'status', width: 90, render: (s) => {COUPON_STATUS_LABELS[s] || s} }, { title: '来源', dataIndex: 'sourceProduct', ellipsis: true }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, { title: '操作', width: 80, render: (_, row) => ( ), }, ]; async function handleGrant(values: AdminBenefitGrantRequest) { setGranting(true); try { await request('/admin/benefit/coupons/grant', { method: 'POST', body: JSON.stringify({ phone: values.phone.trim(), amount: values.amount, remark: values.remark?.trim() || undefined, }), }); message.success('权益已发放'); setGrantOpen(false); grantForm.resetFields(); void reload(); } catch (e) { message.error(e instanceof Error ? e.message : '发放失败'); } finally { setGranting(false); } } return (
好客权益券
{ setFilters(v); setPage(1); }}>
setDrawerOpen(false)} extra={ detail && detail.status !== 'VOID' && ( { await request(`/admin/benefit/coupons/${detail.id}/void`, { method: 'POST' }); message.success('已作废'); setDrawerOpen(false); void reload(); }} > ) } > {detail && ( <> {detail.couponNo} {detail.user?.userNo ?? '—'} {detail.user?.phone ?? '—'} {detail.order?.orderNo ?? '—'} ¥{Number(detail.totalAmount).toFixed(2)} ¥{Number(detail.balance).toFixed(2)} {COUPON_STATUS_LABELS[detail.status] || detail.status} {detail.sourceProduct} 权益核销 {detail.redeemSummary ? ( <> {detail.redeemSummary.couponNo} {detail.redeemSummary.status} ¥{Number(detail.redeemSummary.totalAmount).toFixed(2)} ¥{Number(detail.redeemSummary.usedAmount).toFixed(2)} ({detail.redeemSummary.redeemCount} 笔核销单) ¥{Number(detail.redeemSummary.balance).toFixed(2)} ¥{Number(detail.redeemSummary.redeemRecordSum).toFixed(2)} (本券分摊合计) v === 'SECONDARY' ? 次券 : 主券, }, { title: '门店', dataIndex: ['store', 'name'], ellipsis: true, render: (v: string | undefined, row: CouponRedeemRecord) => v ? `${v}${row.store?.cityName ? `(${row.store.cityName})` : ''}` : '—', }, { title: '本券分摊', dataIndex: 'couponAmount', width: 96, render: (v: number | undefined, row: CouponRedeemRecord) => `¥${Number(v ?? row.amount).toFixed(2)}`, }, { title: '核销总额', dataIndex: 'amount', width: 96, render: (v: number) => `¥${Number(v).toFixed(2)}`, }, { title: '结算额', dataIndex: 'settleAmount', width: 96, render: (v: number) => `¥${Number(v).toFixed(2)}`, }, { title: '时间', dataIndex: 'createdAt', width: 148, render: (v: string) => fmtTime(v), }, { title: '操作', width: 64, render: (_: unknown, row: CouponRedeemRecord) => ( ), }, ]} /> ) : ( 暂无核销摘要 )} )} { setRedeemDrawerOpen(false); setRedeemDetail(null); }} destroyOnClose > {redeemDetailLoading ? ( 加载中… ) : redeemDetail ? ( ) : null} ); }