fix(attendance): remove KYC status filter/column, show KYC info col based on event.enable_kyc; fix search input focus loss
- Remove KYC status filter dropdown and status column from attendance list - Conditionally render KYC type and operation columns based on event's enable_kyc field - Add keepPreviousData to useAdminAttendance to prevent input focus loss on filter change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import type { AttendanceRow } from './attendance-list.view';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAdminAttendance } from '@/hooks/data/useAdminAttendance';
|
||||
import { useEventInfo } from '@/hooks/data/useEventInfo';
|
||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
||||
import { canManageEvents } from '@/lib/permissions';
|
||||
import { AttendanceListSkeleton } from './attendance-list.skeleton';
|
||||
@@ -14,8 +15,8 @@ interface AttendanceListContainerProps {
|
||||
interface AttendanceApiRow {
|
||||
attendance_id: string;
|
||||
kyc_type?: string;
|
||||
kyc_status?: string;
|
||||
joined_at?: string;
|
||||
kyc_info?: Record<string, unknown> | null;
|
||||
joined_at?: string | null;
|
||||
checked_in_at?: string | null;
|
||||
user_info: {
|
||||
user_id: string;
|
||||
@@ -25,28 +26,23 @@ interface AttendanceApiRow {
|
||||
};
|
||||
}
|
||||
|
||||
export function mapKycStatusFilterToQuery(status: string | undefined) {
|
||||
if (status === 'approved') {
|
||||
return 'with_kyc';
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
export function AttendanceListContainer({ eventId }: AttendanceListContainerProps) {
|
||||
const { data: userData } = useUserInfo();
|
||||
const permissionLevel = userData.data?.permission_level ?? 0;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: eventInfoData } = useEventInfo(eventId);
|
||||
const enableKyc = (eventInfoData as unknown as { data?: { enable_kyc?: boolean } })?.data?.enable_kyc ?? false;
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [sortBy, setSortBy] = useState('joined_at');
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||
const [nameFilter, setNameFilter] = useState('');
|
||||
const [kycStatusFilter, setKycStatusFilter] = useState<string | undefined>(undefined);
|
||||
|
||||
const { data, isPending } = useAdminAttendance({
|
||||
eventId,
|
||||
name: nameFilter || undefined,
|
||||
kyc_status: mapKycStatusFilterToQuery(kycStatusFilter),
|
||||
page,
|
||||
page_size: 50,
|
||||
sort_by: sortBy,
|
||||
@@ -74,11 +70,6 @@ export function AttendanceListContainer({ eventId }: AttendanceListContainerProp
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
function handleKycStatusFilterChange(status: string | undefined) {
|
||||
setKycStatusFilter(status);
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
if (!canManageEvents(permissionLevel)) {
|
||||
return null;
|
||||
}
|
||||
@@ -95,8 +86,8 @@ export function AttendanceListContainer({ eventId }: AttendanceListContainerProp
|
||||
nickname: row.user_info.nickname ?? '',
|
||||
avatar: row.user_info.avatar ?? '',
|
||||
kyc_type: row.kyc_type ?? null,
|
||||
kyc_status: row.kyc_status ?? null,
|
||||
joined_at: row.joined_at ?? '',
|
||||
kyc_info: row.kyc_info ?? null,
|
||||
joined_at: row.joined_at ?? null,
|
||||
checked_in_at: row.checked_in_at ?? null,
|
||||
}));
|
||||
|
||||
@@ -109,11 +100,10 @@ export function AttendanceListContainer({ eventId }: AttendanceListContainerProp
|
||||
sortBy={sortBy}
|
||||
sortDir={sortDir}
|
||||
nameFilter={nameFilter}
|
||||
kycStatusFilter={kycStatusFilter}
|
||||
enableKyc={enableKyc}
|
||||
onPageChange={setPage}
|
||||
onSortChange={handleSortChange}
|
||||
onNameFilterChange={handleNameFilterChange}
|
||||
onKycStatusFilterChange={handleKycStatusFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type React from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -24,8 +23,8 @@ export interface AttendanceRow {
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
kyc_type: string | null;
|
||||
kyc_status: string | null;
|
||||
joined_at: string;
|
||||
kyc_info: Record<string, unknown> | null;
|
||||
joined_at: string | null;
|
||||
checked_in_at: string | null;
|
||||
}
|
||||
|
||||
@@ -37,11 +36,10 @@ interface AttendanceListViewProps {
|
||||
sortBy: string;
|
||||
sortDir: 'asc' | 'desc';
|
||||
nameFilter: string;
|
||||
kycStatusFilter: string | undefined;
|
||||
enableKyc: boolean;
|
||||
onPageChange: (page: number) => void;
|
||||
onSortChange: (column: string) => void;
|
||||
onNameFilterChange: (name: string) => void;
|
||||
onKycStatusFilterChange: (status: string | undefined) => void;
|
||||
}
|
||||
|
||||
interface SortableHeaderProps {
|
||||
@@ -67,26 +65,16 @@ function SortableHeader({ column, sortBy, sortDir, onSortChange, children }: Sor
|
||||
);
|
||||
}
|
||||
|
||||
function kycStatusBadge(status: string | null) {
|
||||
if (status === null)
|
||||
return <span className="text-muted-foreground">—</span>;
|
||||
const map: Record<string, { variant: 'secondary' | 'default' | 'destructive'; label: string }> = {
|
||||
pending: { variant: 'secondary', label: '待审核' },
|
||||
approved: { variant: 'default', label: '已通过' },
|
||||
rejected: { variant: 'destructive', label: '已驳回' },
|
||||
};
|
||||
const entry = map[status];
|
||||
if (entry === undefined)
|
||||
return <span className="text-muted-foreground">{status}</span>;
|
||||
return <Badge variant={entry.variant}>{entry.label}</Badge>;
|
||||
}
|
||||
const KYC_TYPE_MAP: Record<string, string> = {
|
||||
cnrid: '身份证',
|
||||
passport: '护照',
|
||||
};
|
||||
|
||||
const KYC_STATUS_OPTIONS = [
|
||||
{ label: '全部', value: '__all__' },
|
||||
{ label: '待审核', value: 'pending' },
|
||||
{ label: '已通过', value: 'approved' },
|
||||
{ label: '已驳回', value: 'rejected' },
|
||||
] as const;
|
||||
function kycTypeLabel(type: string | null) {
|
||||
if (type === null)
|
||||
return <span className="text-muted-foreground">—</span>;
|
||||
return <span>{KYC_TYPE_MAP[type] ?? type}</span>;
|
||||
}
|
||||
|
||||
export function AttendanceListView({
|
||||
attendees,
|
||||
@@ -96,13 +84,13 @@ export function AttendanceListView({
|
||||
sortBy,
|
||||
sortDir,
|
||||
nameFilter,
|
||||
kycStatusFilter,
|
||||
enableKyc,
|
||||
onPageChange,
|
||||
onSortChange,
|
||||
onNameFilterChange,
|
||||
onKycStatusFilterChange,
|
||||
}: AttendanceListViewProps) {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const [kycInfoTarget, setKycInfoTarget] = useState<AttendanceRow | null>(null);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4 p-4 lg:p-6">
|
||||
@@ -115,21 +103,6 @@ export function AttendanceListView({
|
||||
value={nameFilter}
|
||||
onChange={e => onNameFilterChange(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
value={kycStatusFilter ?? '__all__'}
|
||||
onValueChange={val => onKycStatusFilterChange(val === '__all__' ? undefined : val)}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{KYC_STATUS_OPTIONS.map(opt => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
@@ -137,10 +110,10 @@ export function AttendanceListView({
|
||||
<TableRow>
|
||||
<TableHead className="w-10" />
|
||||
<SortableHeader column="username" sortBy={sortBy} sortDir={sortDir} onSortChange={onSortChange}>用户名</SortableHeader>
|
||||
<TableHead>KYC 类型</TableHead>
|
||||
<TableHead>KYC 状态</TableHead>
|
||||
{enableKyc && <TableHead>KYC 类型</TableHead>}
|
||||
<SortableHeader column="joined_at" sortBy={sortBy} sortDir={sortDir} onSortChange={onSortChange}>报名时间</SortableHeader>
|
||||
<SortableHeader column="checked_in_at" sortBy={sortBy} sortDir={sortDir} onSortChange={onSortChange}>签到时间</SortableHeader>
|
||||
{enableKyc && <TableHead>操作</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -155,20 +128,31 @@ export function AttendanceListView({
|
||||
<div className="font-medium">{row.username}</div>
|
||||
<div className="text-xs text-muted-foreground">{row.nickname}</div>
|
||||
</TableCell>
|
||||
{enableKyc && (
|
||||
<TableCell className="text-muted-foreground">
|
||||
{kycTypeLabel(row.kyc_type)}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-muted-foreground">
|
||||
{row.kyc_type ?? '—'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{kycStatusBadge(row.kyc_status)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(row.joined_at).toLocaleDateString('zh-CN')}
|
||||
{row.joined_at ? new Date(row.joined_at).toLocaleDateString('zh-CN') : '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{row.checked_in_at !== null
|
||||
? new Date(row.checked_in_at).toLocaleDateString('zh-CN')
|
||||
: '未签到'}
|
||||
</TableCell>
|
||||
{enableKyc && (
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={row.kyc_info === null}
|
||||
onClick={() => setKycInfoTarget(row)}
|
||||
>
|
||||
展示 KYC 信息
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -201,6 +185,23 @@ export function AttendanceListView({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={kycInfoTarget !== null} onOpenChange={open => !open && setKycInfoTarget(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
KYC 信息 —
|
||||
{' '}
|
||||
{kycInfoTarget?.username}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-auto rounded-md bg-muted p-4 text-sm">
|
||||
{kycInfoTarget?.kyc_info !== null && kycInfoTarget?.kyc_info !== undefined
|
||||
? JSON.stringify(kycInfoTarget.kyc_info, null, 2)
|
||||
: '无 KYC 信息'}
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
import { getEventAttendanceOptions } from '@/client/@tanstack/react-query.gen';
|
||||
|
||||
export interface AdminAttendanceParams {
|
||||
@@ -14,15 +14,18 @@ export interface AdminAttendanceParams {
|
||||
export function useAdminAttendance(params: AdminAttendanceParams) {
|
||||
const page = params.page ?? 1;
|
||||
const pageSize = params.page_size ?? 50;
|
||||
return useQuery(getEventAttendanceOptions({
|
||||
query: {
|
||||
event_id: params.eventId,
|
||||
name: params.name,
|
||||
kyc_status: params.kyc_status,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
sort_by: params.sort_by,
|
||||
sort_order: params.sort_dir,
|
||||
},
|
||||
}));
|
||||
return useQuery({
|
||||
...getEventAttendanceOptions({
|
||||
query: {
|
||||
event_id: params.eventId,
|
||||
name: params.name,
|
||||
kyc_status: params.kyc_status,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
sort_by: params.sort_by,
|
||||
sort_order: params.sort_dir,
|
||||
},
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user