399 lines
15 KiB
TypeScript
399 lines
15 KiB
TypeScript
|
|
'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>
|
||
|
|
);
|
||
|
|
}
|