import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, Descriptions, Drawer, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, 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, DELIVERY_TYPE_LABELS, fmtTime } from '../lib/constants'; import RedeemRecordDetailDescriptions from '../components/RedeemRecordDetailDescriptions'; import { useAdminList } from '../lib/useAdminList'; import { useAdminListColumns } from '../lib/useAdminListColumns'; import { AdminListHeader } from '../components/AdminListHeader'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; type CouponOrder = { id?: string; orderNo?: string; productName?: string | null; productSpec?: string | null; quantity?: number | null; saleUnit?: string | null; deliveryType?: string | null; payAmount?: number | string | null; }; type Row = { id: string; couponNo: string; totalAmount: number; balance: number; usedAmount: number; status: string; sourceProduct: string; createdAt: string; user?: { id?: string; userNo: string; phone: string | null }; order?: CouponOrder | null; }; function saleUnitLabel(unit?: string | null) { if (unit === 'BOX') return '箱'; if (unit === 'BOTTLE') return '瓶'; return ''; } function formatPayAmount(v?: number | string | null) { if (v == null || v === '') return ''; const n = Number(v); return Number.isFinite(n) ? `¥${n.toFixed(2)}` : ''; } /** 来源:有订单时展示商品名、规格、数量、配送方式、金额;手动发放沿用 sourceProduct */ function formatCouponSource(row: { sourceProduct?: string; order?: CouponOrder | null }) { const order = row.order; if (!order?.orderNo && !order?.productName) { return row.sourceProduct || '—'; } const unit = saleUnitLabel(order.saleUnit); const qty = order.quantity != null ? `${order.quantity}${unit}` : ''; const delivery = DELIVERY_TYPE_LABELS[order.deliveryType ?? ''] || order.deliveryType || ''; const amount = formatPayAmount(order.payAmount); const parts = [ order.productName || row.sourceProduct, order.productSpec, qty, delivery, amount, ].filter((p) => p != null && String(p).trim() !== ''); return parts.join(' / ') || row.sourceProduct || '—'; } 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[]; }; export default function BenefitCouponsPage() { const navigate = useNavigate(); 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 baseColumns: ColumnsType = [ { title: '券号', dataIndex: 'couponNo', width: 200, ellipsis: false, render: (v, row) => ( { setDetail(await request(`/admin/benefit/coupons/${row.id}`)); setDrawerOpen(true); }} > {v} ), }, { title: '用户', dataIndex: ['user', 'userNo'], width: 120, ellipsis: false, render: (v: string | undefined, row) => row.user?.id ? ( navigate('/users', { state: { openUserId: String(row.user!.id) } })}> {v} ) : ( v || '—' ), }, { title: '手机号', dataIndex: ['user', 'phone'], width: 120, render: (v) => v || '—' }, { title: '订单', dataIndex: ['order', 'orderNo'], width: 180, ellipsis: false, render: (v: string | undefined) => v ? ( navigate(`/orders?orderNo=${encodeURIComponent(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', width: 360, ellipsis: false, render: (_: string, row) => formatCouponSource(row), }, { 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); } } const { columns, settingsButton, settingsModal } = useAdminListColumns('benefit-coupons', baseColumns, { page, pageSize }); return (
{settingsModal} setGrantOpen(true)}> 手动发放 } />
{ 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?.id ? ( navigate('/users', { state: { openUserId: String(detail.user!.id) } })} > {detail.user?.userNo} ) : ( (detail.user?.userNo ?? '—') )} {detail.user?.phone ?? '—'} {detail.order?.orderNo ? ( navigate(`/orders?orderNo=${encodeURIComponent(detail.order!.orderNo!)}`) } > {detail.order.orderNo} ) : ( '—' )} ¥{Number(detail.totalAmount).toFixed(2)} ¥{Number(detail.balance).toFixed(2)} {COUPON_STATUS_LABELS[detail.status] || detail.status} {formatCouponSource(detail)} 权益核销 {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'], 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} ); }