diff options
Diffstat (limited to 'components/form-data-stat')
| -rw-r--r-- | components/form-data-stat/form-data-stat-table.tsx | 261 |
1 files changed, 219 insertions, 42 deletions
diff --git a/components/form-data-stat/form-data-stat-table.tsx b/components/form-data-stat/form-data-stat-table.tsx index fe59785d..a56a4e88 100644 --- a/components/form-data-stat/form-data-stat-table.tsx +++ b/components/form-data-stat/form-data-stat-table.tsx @@ -1,10 +1,11 @@ +// components/form-data-stat/form-data-stat-table.tsx "use client"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { RefreshCw } from "lucide-react"; +import { RefreshCw, Check, ChevronsUpDown } from "lucide-react"; import { type ColumnDef } from "@tanstack/react-table"; import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"; import { ClientDataTable } from "@/components/client-data-table/data-table"; @@ -12,17 +13,25 @@ import { cn } from "@/lib/utils"; import { toast } from "sonner"; import type { DataTableAdvancedFilterField } from "@/types/table"; import { Progress } from "@/components/ui/progress"; -import { getVendorFormStatus } from "@/lib/forms/stat"; +import { getVendorFormStatus, getProjectsWithContracts } from "@/lib/forms/stat"; +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; +import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command"; // 타입 정의 interface VendorFormStatus { vendorId: number; vendorName: string; - formCount: number; // 벤더가 가진 form 개수 - tagCount: number; // 벤더가 가진 tag 개수 - totalFields: number; // 입력해야 하는 총 필드 개수 - completedFields: number; // 입력 완료된 필드 개수 - completionRate: number; // 완료율 (%) + formCount: number; + tagCount: number; + totalFields: number; + completedFields: number; + completionRate: number; +} + +interface Project { + id: number; + projectCode: string; + projectName: string; } interface VendorFormStatusTableProps { @@ -45,35 +54,164 @@ const getCompletionBadgeVariant = (rate: number): "default" | "secondary" | "des return "destructive"; }; +// 프로젝트 선택 컴포넌트 +function ProjectSelectorWithContracts({ + selectedProject, + onProjectSelect, + projects, + isLoading +}: { + selectedProject: Project | null; + onProjectSelect: (project: Project | null) => void; + projects: Project[]; + isLoading: boolean; +}) { + const [open, setOpen] = React.useState(false); + const [searchTerm, setSearchTerm] = React.useState(""); + + // 검색어로 필터링 + const filteredProjects = React.useMemo(() => { + if (!searchTerm.trim()) return projects; + + const lowerSearch = searchTerm.toLowerCase(); + return projects.filter( + project => + project.projectCode.toLowerCase().includes(lowerSearch) || + project.projectName.toLowerCase().includes(lowerSearch) + ); + }, [projects, searchTerm]); + + const handleSelectProject = (project: Project | null) => { + onProjectSelect(project); + setOpen(false); + }; + + return ( + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + className="w-full justify-between" + disabled={isLoading} + > + {selectedProject + ? `${selectedProject.projectCode} - ${selectedProject.projectName}` + : "프로젝트를 선택하세요..."} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput + placeholder="프로젝트 코드/이름 검색..." + onValueChange={setSearchTerm} + /> + <CommandList className="max-h-[300px]"> + <CommandEmpty>검색 결과가 없습니다</CommandEmpty> + {isLoading ? ( + <div className="py-6 text-center text-sm">로딩 중...</div> + ) : ( + <CommandGroup> + {/* 전체 옵션 추가 */} + <CommandItem + value="all" + onSelect={() => handleSelectProject(null)} + > + <Check + className={cn( + "mr-2 h-4 w-4", + selectedProject === null ? "opacity-100" : "opacity-0" + )} + /> + <span className="font-medium">전체 프로젝트</span> + </CommandItem> + {/* 프로젝트 목록 */} + {filteredProjects.map((project) => ( + <CommandItem + key={project.id} + value={`${project.projectCode} ${project.projectName}`} + onSelect={() => handleSelectProject(project)} + > + <Check + className={cn( + "mr-2 h-4 w-4", + selectedProject?.id === project.id + ? "opacity-100" + : "opacity-0" + )} + /> + <span className="font-medium">{project.projectCode}</span> + <span className="ml-2 text-gray-500 truncate">- {project.projectName}</span> + </CommandItem> + ))} + </CommandGroup> + )} + </CommandList> + </Command> + </PopoverContent> + </Popover> + ); +} + export function VendorFormStatusTable({ initialData = [], }: VendorFormStatusTableProps) { const [data, setData] = React.useState<VendorFormStatus[]>(initialData); const [isRefreshing, setIsRefreshing] = React.useState(false); + const [selectedProject, setSelectedProject] = React.useState<Project | null>(null); + const [projects, setProjects] = React.useState<Project[]>([]); + const [isLoadingProjects, setIsLoadingProjects] = React.useState(false); + + // 프로젝트 목록 로드 + React.useEffect(() => { + const loadProjects = async () => { + setIsLoadingProjects(true); + try { + const projectsWithContracts = await getProjectsWithContracts(); + setProjects(projectsWithContracts); + + // 프로젝트가 하나만 있으면 자동 선택 + if (projectsWithContracts.length === 1) { + setSelectedProject(projectsWithContracts[0]); + } + } catch (error) { + console.error("프로젝트 목록 로드 오류:", error); + toast.error("프로젝트 목록을 불러오는데 실패했습니다."); + } finally { + setIsLoadingProjects(false); + } + }; + + loadProjects(); + }, []); + + // 프로젝트 변경 시 데이터 로드 + React.useEffect(() => { + handleRefresh(); + }, [selectedProject]); // 데이터 새로고침 const handleRefresh = React.useCallback(async () => { setIsRefreshing(true); try { - const result = await getVendorFormStatus(); + const result = await getVendorFormStatus(selectedProject?.id); setData(result); - toast.success("데이터를 새로고침했습니다."); + if (selectedProject) { + toast.success(`${selectedProject.projectCode} 데이터를 새로고침했습니다.`); + } else { + toast.success("전체 프로젝트 데이터를 새로고침했습니다."); + } } catch (error) { console.error("Refresh error:", error); toast.error("새로고침 중 오류가 발생했습니다."); } finally { setIsRefreshing(false); } - }, []); + }, [selectedProject]); - // 초기 데이터 로드 - React.useEffect(() => { - if (initialData.length === 0) { - handleRefresh(); - } - }, []); - - // 컬럼 정의 + // 컬럼 정의 (기존과 동일) const columns: ColumnDef<VendorFormStatus>[] = React.useMemo(() => [ { accessorKey: "vendorName", @@ -197,6 +335,39 @@ export function VendorFormStatusTable({ return ( <div className={cn("w-full space-y-4")}> + {/* 프로젝트 선택 */} + <Card> + <CardHeader> + <CardTitle>프로젝트 선택</CardTitle> + </CardHeader> + <CardContent> + <div className="flex items-center gap-4"> + <div className="flex-1"> + <ProjectSelectorWithContracts + selectedProject={selectedProject} + onProjectSelect={setSelectedProject} + projects={projects} + isLoading={isLoadingProjects} + /> + </div> + <Button + variant="outline" + size="sm" + onClick={handleRefresh} + disabled={isRefreshing} + > + <RefreshCw className={cn("h-4 w-4 mr-2", isRefreshing && "animate-spin")} /> + 새로고침 + </Button> + </div> + {selectedProject && ( + <div className="mt-2 text-sm text-muted-foreground"> + 선택된 프로젝트: {selectedProject.projectCode} - {selectedProject.projectName} + </div> + )} + </CardContent> + </Card> + {/* 요약 카드 */} <div className="grid gap-4 md:grid-cols-5"> <Card> @@ -249,33 +420,39 @@ export function VendorFormStatusTable({ <Card> <CardHeader> <div className="flex items-center justify-between"> - <CardTitle>벤더별 Form 입력 현황</CardTitle> - <Button - variant="outline" - size="sm" - onClick={handleRefresh} - disabled={isRefreshing} - > - <RefreshCw className={cn("h-4 w-4 mr-2", isRefreshing && "animate-spin")} /> - 새로고침 - </Button> + <CardTitle> + 벤더별 Form 입력 현황 + {selectedProject && ( + <span className="ml-2 text-sm font-normal text-muted-foreground"> + ({selectedProject.projectCode}) + </span> + )} + </CardTitle> </div> </CardHeader> <CardContent> - <ClientDataTable - columns={columns} - data={data} - advancedFilterFields={advancedFilterFields} - autoSizeColumns={true} - compact={true} - maxHeight="34rem" - initialColumnPinning={{ - left: ["vendorName"], - }} - defaultSorting={[ - { id: "completionRate", desc: false }, // 완료율 낮은 순으로 정렬 - ]} - /> + {data.length > 0 ? ( + <ClientDataTable + columns={columns} + data={data} + advancedFilterFields={advancedFilterFields} + autoSizeColumns={true} + compact={true} + maxHeight="34rem" + initialColumnPinning={{ + left: ["vendorName"], + }} + defaultSorting={[ + { id: "completionRate", desc: false }, // 완료율 낮은 순으로 정렬 + ]} + /> + ) : ( + <div className="text-center py-8 text-muted-foreground"> + {selectedProject + ? "선택한 프로젝트에 대한 데이터가 없습니다." + : "데이터가 없습니다."} + </div> + )} </CardContent> </Card> </div> |
