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:
2026-03-27 18:43:58 +08:00
parent b9d12637b7
commit ef4fa016e6
3 changed files with 82 additions and 88 deletions

View File

@@ -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}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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,
});
}