casualroom/apps/fenghuo/web/components/profile/elite-sheet.tsx

399 lines
15 KiB
TypeScript
Raw Permalink Normal View History

2025-07-28 07:50:50 +08:00
'use client';
import * as React from 'react';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
} from '@nice/ui/components/sheet';
import { Separator } from '@nice/ui/components/separator';
import { Card, CardContent, CardHeader, CardTitle } from '@nice/ui/components/card';
import {
IconBuilding,
IconStarFilled,
IconUsers
} from '@tabler/icons-react';
import { cn } from '@nice/ui/lib/utils';
import { EliteFormData } from '@fenghuo/common';
import { DutyLevel } from '@fenghuo/common/enum';
import { useTRPC } from '@fenghuo/client';
import { useQuery } from '@tanstack/react-query';
function getDutyLevelName(level: number): string {
switch (level) {
case 1:
return DutyLevel.PRIMARY;
case 2:
return DutyLevel.MIDDLE;
case 3:
return DutyLevel.HIGH;
default:
return '';
}
}
// 职务等级星星显示组件
function DutyLevelStars({ level }: { level: number }) {
return (
<div className="flex items-center">
{Array.from({ length: 3 }, (_, i) => (
<span key={i}>
{i < level ? (
<IconStarFilled className="size-2.5 text-yellow-500" />
) : (
null
)}
</span>
))}
</div>
);
}
// 人员信息显示组件
function PersonCard({
person,
onViewDetail
}: {
person: {
id: string;
name: string;
dutyName: string;
dutyCode: string;
dutyLevel: number;
};
onViewDetail: (id: string) => void;
}) {
return (
<div
className="flex flex-col items-center justify-center p-2 bg-muted/30 rounded cursor-pointer hover:bg-muted/50 transition-colors min-w-0 min-h-[60px]"
onClick={() => onViewDetail(person.id)}
>
<div className="text-center space-y-1">
<div className="font-medium text-sm truncate w-full">{person.name}
<span className="text-xs text-muted-foreground"> #{person.dutyCode}</span>
</div>
{
person.dutyLevel > 0 && (
<div className="flex items-center justify-center gap-1">
<div className="text-xs text-muted-foreground">{getDutyLevelName(person.dutyLevel)}</div>
<DutyLevelStars level={person.dutyLevel} />
</div>
)
}
</div>
</div>
);
}
// 职业统计函数 - 计算职业内各等级人数
function getProfessionLevelStats(persons: Array<{ dutyLevel: number }>) {
return persons.reduce((acc, person) => {
const level = person.dutyLevel;
acc[level] = (acc[level] || 0) + 1;
return acc;
}, {} as Record<number, number>);
}
// 渲染职业等级统计
function renderProfessionLevelStats(levelStats: Record<number, number>) {
const stats: string[] = [];
if (levelStats?.[3]) stats.push(`首席教练员: ${levelStats[3]}`);
if (levelStats?.[2]) stats.push(`教练员: ${levelStats[2]}`);
if (levelStats?.[1]) stats.push(`助理教练员: ${levelStats[1]}`);
return stats.length > 0 ? (
<span className="text-xs text-muted-foreground">
({stats.join(', ')})
</span>
) : null;
}
// 台站板块组件
function StationSection({
stationName,
data,
onViewDetail
}: {
stationName: string;
data: {
technicians: Array<{
id: string;
name: string;
dutyName: string;
dutyCode: string;
dutyLevel: number;
}>;
supervisors: Array<{
id: string;
name: string;
dutyName: string;
dutyCode: string;
dutyLevel: number;
}>;
operators: Array<{
id: string;
name: string;
dutyName: string;
dutyCode: string;
dutyLevel: number;
}>;
};
onViewDetail: (id: string) => void;
}) {
const totalCount = data.technicians.length + data.supervisors.length + data.operators.length;
// 统计各等级人数
const allPersons = [...data.technicians, ...data.supervisors, ...data.operators];
const levelStats = allPersons.reduce((acc, person) => {
const level = person.dutyLevel;
acc[level] = (acc[level] || 0) + 1;
return acc;
}, {} as Record<number, number>);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 font-normal text-base">
<IconBuilding className="size-4" />
{stationName}
</CardTitle>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<IconUsers className="size-4" />
<span>: {totalCount} </span>
</div>
{Object.entries(levelStats).reverse().map(([level, count]) => {
if (Number(level) > 0) {
return (
<div key={level} className="flex items-center gap-1">
{getDutyLevelName(Number(level))}
<span>: {count}</span>
</div>
)
}
return null
})}
</div>
</CardHeader>
{
totalCount > 0 ? (
<CardContent className="space-y-4">
{/* 技师 */}
{data.technicians.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm text-muted-foreground">{data.technicians.length}</span>
{renderProfessionLevelStats(getProfessionLevelStats(data.technicians))}
</div>
<div className="grid grid-cols-4 gap-2">
{data.technicians.map((person) => (
<PersonCard
key={person.id}
person={person}
onViewDetail={onViewDetail}
/>
))}
</div>
</div>
)}
{/* 领班员/台站长 */}
{data.supervisors.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<span className="text-sm text-muted-foreground">/</span>
<span className="text-sm text-muted-foreground">{data.supervisors.length}</span>
{renderProfessionLevelStats(getProfessionLevelStats(data.supervisors))}
</div>
<div className="grid grid-cols-4 gap-2">
{data.supervisors.map((person) => (
<PersonCard
key={person.id}
person={person}
onViewDetail={onViewDetail}
/>
))}
</div>
</div>
)}
{/* 值机员 */}
{data.operators.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm text-muted-foreground">{data.operators.length}</span>
{renderProfessionLevelStats(getProfessionLevelStats(data.operators))}
</div>
<div className="grid grid-cols-4 gap-2">
{data.operators.map((person) => (
<PersonCard
key={person.id}
person={person}
onViewDetail={onViewDetail}
/>
))}
</div>
</div>
)}
</CardContent>
) : (
<CardContent className="text-center py-4 text-muted-foreground text-sm">
</CardContent>
)
}
</Card>
);
}
export interface EliteSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string; // 修改为通用标题
professionId?: string; // 可选的专业ID
stationId?: string; // 可选的台站ID
onViewDetail: (personId: string) => void;
}
export function EliteSheet({
open,
onOpenChange,
title,
professionId,
stationId,
onViewDetail
}: EliteSheetProps) {
const trpc = useTRPC();
// 根据传入的ID动态获取数据
const { data: sheetData, isLoading } = useQuery({
...trpc.profile.findElite.queryOptions({
professionIds: professionId ? [professionId] : undefined,
stationIds: stationId ? [stationId] : undefined,
page: 1,
pageSize: 1000, // 获取所有数据
}),
enabled: open, // 只有当Sheet打开时才获取数据
});
// 按台站分组数据
const stationData = React.useMemo(() => {
const groupedByStation: Record<string, EliteFormData> = {};
sheetData?.data.forEach((item) => {
const stationName = item.station;
const parentOrganizationName = item.parentOrganization;
// 使用父级组织名称和台站名称组合作为key
let groupKey = '';
if (!parentOrganizationName) {
groupKey = stationName;
} else {
groupKey = `${parentOrganizationName}/${stationName}`;
}
if (!groupedByStation[groupKey]) {
groupedByStation[groupKey] = {
sequence: item.sequence,
profession: item.profession,
professionId: item.professionId, // 添加专业ID
station: item.station,
stationId: item.stationId, // 添加台站ID
parentOrganization: item.parentOrganization,
technicians: [],
supervisors: [],
operators: []
};
}
// 合并人员数据
groupedByStation[groupKey]!.technicians.push(...item.technicians);
groupedByStation[groupKey]!.supervisors.push(...item.supervisors);
groupedByStation[groupKey]!.operators.push(...item.operators);
});
return groupedByStation;
}, [sheetData?.data]);
// 计算总统计信息
const totalStats = React.useMemo(() => {
const allPersons = sheetData?.data.flatMap(item => [
...item.technicians,
...item.supervisors,
...item.operators
]) || [];
const totalCount = allPersons.length;
const levelStats = allPersons.reduce((acc, person) => {
const level = person.dutyLevel;
acc[level] = (acc[level] || 0) + 1;
return acc;
}, {} as Record<number, number>);
return { totalCount, levelStats };
}, [sheetData?.data]);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
className={cn(
"w-full sm:w-1/3 sm:max-w-none overflow-y-auto p-4",
"data-[state=closed]:duration-300 data-[state=open]:duration-500"
)}
>
<SheetHeader className='p-0'>
<SheetTitle className="text-lg font-bold">
{title}
</SheetTitle>
<SheetDescription className="flex items-center text-sm gap-4">
<span className="flex items-center gap-2">
<IconUsers className="size-4" />
<span>: {totalStats.totalCount} </span>
</span>
{Object.entries(totalStats.levelStats).reverse().map(([level, count]) => {
if (Number(level) > 0) {
return (
<span key={level} className="flex items-center gap-1">
<span>{getDutyLevelName(Number(level))}</span>
<span>: {count}</span>
</span>
)
} else {
return null
}
})}
</SheetDescription>
</SheetHeader>
<Separator />
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">
...
</div>
) : (
<div className='space-y-4'>
{Object.entries(stationData).map(([stationName, stationInfo]) => (
<StationSection
key={stationName}
stationName={stationName}
data={stationInfo}
onViewDetail={onViewDetail}
/>
))}
{Object.keys(stationData).length === 0 && (
<div className="text-center py-8 text-muted-foreground">
</div>
)}
</div>
)}
</SheetContent>
</Sheet>
);
}