diff options
Diffstat (limited to 'lib/users/access-control/domain-stats-cards.tsx')
| -rw-r--r-- | lib/users/access-control/domain-stats-cards.tsx | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/lib/users/access-control/domain-stats-cards.tsx b/lib/users/access-control/domain-stats-cards.tsx new file mode 100644 index 00000000..e6320e12 --- /dev/null +++ b/lib/users/access-control/domain-stats-cards.tsx @@ -0,0 +1,232 @@ +// components/domain-stats-cards.tsx +"use client" + +import * as React from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Users, + Clock, + Shield, + ShoppingCart, + TrendingUp, + Settings, + Building, + AlertCircle +} from "lucide-react" +import { toast } from "sonner" +import { getUserDomainStats } from "../service" + +interface DomainStatsCardsProps { + onDomainFilter: (domain: string | null) => void + currentFilter?: string | null +} + +// 도메인별 설정 +const domainConfig = { + pending: { + label: "승인 대기", + description: "신규 사용자", + icon: Clock, + color: "bg-yellow-500", + textColor: "text-yellow-700", + bgColor: "bg-yellow-50", + borderColor: "border-yellow-200" + }, + evcp: { + label: "전체 시스템", + description: "관리자급", + icon: Shield, + color: "bg-blue-500", + textColor: "text-blue-700", + bgColor: "bg-blue-50", + borderColor: "border-blue-200" + }, + procurement: { + label: "구매관리팀", + description: "구매/계약 관리", + icon: ShoppingCart, + color: "bg-green-500", + textColor: "text-green-700", + bgColor: "bg-green-50", + borderColor: "border-green-200" + }, + sales: { + label: "기술영업팀", + description: "영업/프로젝트", + icon: TrendingUp, + color: "bg-purple-500", + textColor: "text-purple-700", + bgColor: "bg-purple-50", + borderColor: "border-purple-200" + }, + engineering: { + label: "설계관리팀", + description: "설계/기술평가", + icon: Settings, + color: "bg-orange-500", + textColor: "text-orange-700", + bgColor: "bg-orange-50", + borderColor: "border-orange-200" + }, + // partners: { + // label: "협력업체", + // description: "외부 업체", + // icon: Building, + // color: "bg-indigo-500", + // textColor: "text-indigo-700", + // bgColor: "bg-indigo-50", + // borderColor: "border-indigo-200" + // } +} + +interface DomainStats { + domain: string + count: number +} + +export function DomainStatsCards({ onDomainFilter, currentFilter }: DomainStatsCardsProps) { + const [stats, setStats] = React.useState<DomainStats[]>([]) + const [isLoading, setIsLoading] = React.useState(true) + const [totalUsers, setTotalUsers] = React.useState(0) + + // 통계 데이터 로드 + React.useEffect(() => { + const loadStats = async () => { + setIsLoading(true) + try { + const result = await getUserDomainStats() + if (result.success) { + setStats(result.data) + setTotalUsers(result.data.reduce((sum, item) => sum + item.count, 0)) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("통계 로드 오류:", error) + toast.error("통계를 불러오는 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + loadStats() + }, []) + + // 도메인별 카드 클릭 핸들러 + const handleDomainClick = (domain: string) => { + if (currentFilter === domain) { + // 이미 선택된 도메인이면 필터 해제 + onDomainFilter(null) + } else { + // 새로운 도메인 필터 적용 + onDomainFilter(domain) + } + } + + // pending 사용자 수 가져오기 + const pendingCount = stats.find(s => s.domain === "pending")?.count || 0 + + if (isLoading) { + return ( + <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4"> + {Array.from({ length: 6 }).map((_, i) => ( + <Card key={i} className="animate-pulse"> + <CardContent className="p-4"> + <div className="h-16 bg-gray-200 rounded"></div> + </CardContent> + </Card> + ))} + </div> + ) + } + + return ( + <div className="space-y-4"> + {/* 요약 정보 */} + <div className="flex items-center justify-between"> + <div> + <h3 className="text-lg font-semibold">도메인별 사용자 현황</h3> + <p className="text-sm text-muted-foreground"> + 총 {totalUsers}명의 사용자가 등록되어 있습니다. + </p> + </div> + + {pendingCount > 0 && ( + <Button + variant="outline" + size="sm" + onClick={() => handleDomainClick("pending")} + className={`gap-2 ${currentFilter === "pending" ? "bg-yellow-50 border-yellow-300" : ""}`} + > + <AlertCircle className="h-4 w-4 text-yellow-600" /> + 승인 대기 {pendingCount}명 + </Button> + )} + </div> + + {/* 도메인별 통계 카드 */} + <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4"> + {Object.entries(domainConfig).map(([domain, config]) => { + const domainStat = stats.find(s => s.domain === domain) + const count = domainStat?.count || 0 + const isActive = currentFilter === domain + const IconComponent = config.icon + + return ( + <Card + key={domain} + className={`cursor-pointer transition-all hover:shadow-md ${ + isActive + ? `${config.bgColor} ${config.borderColor} border-2` + : "hover:bg-gray-50" + }`} + onClick={() => handleDomainClick(domain)} + > + <CardContent className="p-4"> + <div className="flex items-center space-x-3"> + <div className={`p-2 rounded-full ${config.color}`}> + <IconComponent className="h-4 w-4 text-white" /> + </div> + <div className="flex-1 min-w-0"> + <div className="flex items-center justify-between"> + <p className={`text-sm font-medium ${isActive ? config.textColor : "text-gray-900"}`}> + {config.label} + </p> + <Badge + variant={isActive ? "default" : "secondary"} + className="ml-2" + > + {count} + </Badge> + </div> + <p className="text-xs text-muted-foreground truncate"> + {config.description} + </p> + </div> + </div> + </CardContent> + </Card> + ) + })} + </div> + + {/* 필터 상태 표시 */} + {currentFilter && ( + <div className="flex items-center gap-2"> + <span className="text-sm text-muted-foreground">필터 적용됨:</span> + <Badge variant="outline" className="gap-1"> + {domainConfig[currentFilter as keyof typeof domainConfig]?.label || currentFilter} + <button + onClick={() => onDomainFilter(null)} + className="ml-1 hover:bg-gray-200 rounded-full p-0.5" + > + × + </button> + </Badge> + </div> + )} + </div> + ) +}
\ No newline at end of file |
