summaryrefslogtreecommitdiff
path: root/components/form-data-stat/form-data-stat-table.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-09 10:32:34 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-09 10:32:34 +0000
commitc62ec046327fd388ebce04571b55910747e69a3b (patch)
tree41ccdc4a8dea99808622f6d5d52014ac59a2d7ab /components/form-data-stat/form-data-stat-table.tsx
parentebcec3f296d1d27943caf8a3aed26efef117cdc5 (diff)
(정희성, 최겸, 대표님) formatDate 변경 등
Diffstat (limited to 'components/form-data-stat/form-data-stat-table.tsx')
-rw-r--r--components/form-data-stat/form-data-stat-table.tsx261
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>