summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 00:32:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 00:32:31 +0000
commit20800b214145ee6056f94ca18fa1054f145eb977 (patch)
treeb5c8b27febe5b126e6d9ece115ea05eace33a020 /lib/vendor-document-list/table/enhanced-doc-table-columns.tsx
parente1344a5da1aeef8fbf0f33e1dfd553078c064ccc (diff)
(대표님) lib 파트 커밋
Diffstat (limited to 'lib/vendor-document-list/table/enhanced-doc-table-columns.tsx')
-rw-r--r--lib/vendor-document-list/table/enhanced-doc-table-columns.tsx612
1 files changed, 612 insertions, 0 deletions
diff --git a/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx
new file mode 100644
index 00000000..534a80a0
--- /dev/null
+++ b/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx
@@ -0,0 +1,612 @@
+// updated-enhanced-doc-table-columns.tsx
+"use client"
+
+import * as React from "react"
+import { ColumnDef } from "@tanstack/react-table"
+import { formatDate, formatDateTime } from "@/lib/utils"
+import { Checkbox } from "@/components/ui/checkbox"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { DataTableRowAction } from "@/types/table"
+import { EnhancedDocumentsView } from "@/db/schema/vendorDocu"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Progress } from "@/components/ui/progress"
+import {
+ Ellipsis,
+ AlertTriangle,
+ Clock,
+ CheckCircle,
+ Upload,
+ Calendar,
+ User,
+ FileText,
+ Eye,
+ Edit,
+ Trash2
+} from "lucide-react"
+import { cn } from "@/lib/utils"
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<EnhancedDocumentsView> | null>>
+ projectType: string | null
+}
+
+// 유틸리티 함수들
+const getStatusColor = (status: string, isOverdue = false) => {
+ if (isOverdue) return 'destructive'
+ switch (status) {
+ case 'COMPLETED': case 'APPROVED': return 'success'
+ case 'IN_PROGRESS': return 'default'
+ case 'SUBMITTED': case 'UNDER_REVIEW': return 'secondary'
+ case 'REJECTED': return 'destructive'
+ default: return 'outline'
+ }
+}
+
+const getPriorityColor = (priority: string) => {
+ switch (priority) {
+ case 'HIGH': return 'destructive'
+ case 'MEDIUM': return 'default'
+ case 'LOW': return 'secondary'
+ default: return 'outline'
+ }
+}
+
+const getStatusText = (status: string) => {
+ switch (status) {
+ case 'PLANNED': return '계획됨'
+ case 'IN_PROGRESS': return '진행중'
+ case 'SUBMITTED': return '제출됨'
+ case 'UNDER_REVIEW': return '검토중'
+ case 'APPROVED': return '승인됨'
+ case 'REJECTED': return '반려됨'
+ case 'COMPLETED': return '완료됨'
+ default: return status
+ }
+}
+
+const getPriorityText = (priority: string) => {
+ switch (priority) {
+ case 'HIGH': return '높음'
+ case 'MEDIUM': return '보통'
+ case 'LOW': return '낮음'
+ default: return priority
+ }
+}
+
+// 마감일 정보 컴포넌트
+const DueDateInfo = ({
+ daysUntilDue,
+ isOverdue,
+ className = ""
+}: {
+ daysUntilDue: number | null
+ isOverdue: boolean
+ className?: string
+}) => {
+ if (isOverdue && daysUntilDue !== null && daysUntilDue < 0) {
+ return (
+ <div className={cn("flex items-center gap-1 text-red-600", className)}>
+ <AlertTriangle className="w-4 h-4" />
+ <span className="text-sm font-medium">{Math.abs(daysUntilDue)}일 지연</span>
+ </div>
+ )
+ }
+
+ if (daysUntilDue === 0) {
+ return (
+ <div className={cn("flex items-center gap-1 text-orange-600", className)}>
+ <Clock className="w-4 h-4" />
+ <span className="text-sm font-medium">오늘 마감</span>
+ </div>
+ )
+ }
+
+ if (daysUntilDue && daysUntilDue > 0 && daysUntilDue <= 3) {
+ return (
+ <div className={cn("flex items-center gap-1 text-orange-600", className)}>
+ <Clock className="w-4 h-4" />
+ <span className="text-sm font-medium">{daysUntilDue}일 남음</span>
+ </div>
+ )
+ }
+
+ if (daysUntilDue && daysUntilDue > 0) {
+ return (
+ <div className={cn("flex items-center gap-1 text-gray-600", className)}>
+ <Calendar className="w-4 h-4" />
+ <span className="text-sm">{daysUntilDue}일 남음</span>
+ </div>
+ )
+ }
+
+ return (
+ <div className={cn("flex items-center gap-1 text-green-600", className)}>
+ <CheckCircle className="w-4 h-4" />
+ <span className="text-sm">완료</span>
+ </div>
+ )
+}
+
+export function getUpdatedEnhancedColumns({
+ setRowAction,
+ projectType
+}: GetColumnsProps): ColumnDef<EnhancedDocumentsView>[] {
+ return [
+ // 체크박스 선택
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ size: 40,
+ enableSorting: false,
+ enableHiding: false,
+ },
+
+ // 문서번호 + 우선순위
+ {
+ accessorKey: "docNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="문서번호" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ return (
+ <div className="flex flex-col gap-1 items-start"> {/* ✅ items-start 추가 */}
+ <span className="font-mono text-sm font-medium">{doc.docNumber}</span>
+ {/* {doc.currentStagePriority && (
+ <Badge variant={getPriorityColor(doc.currentStagePriority)} className="self-start inline-flex w-auto shrink-0 whitespace-nowrap text-xs" >
+ {getPriorityText(doc.currentStagePriority)}
+ </Badge>
+ )} */}
+ </div>
+ )
+ },
+ size: 120,
+ enableResizing: true,
+ meta: {
+ excelHeader: "문서번호"
+ },
+ },
+
+ // 문서명 + 담당자
+ {
+ accessorKey: "title",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="문서명" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ return (
+ <div className="min-w-0 flex-1">
+ <div className="font-medium text-gray-900 truncate" title={doc.title}>
+ {doc.title}
+ </div>
+ <div className="flex items-center gap-2 text-sm text-gray-500 mt-1">
+ {doc.pic && (
+ <span className="text-xs bg-gray-100 px-2 py-0.5 rounded">
+ PIC: {doc.pic}
+ </span>
+ )}
+ {doc.currentStageAssigneeName && (
+ <div className="flex items-center gap-1">
+ <User className="w-3 h-3" />
+ <span>{doc.currentStageAssigneeName}</span>
+ </div>
+ )}
+ </div>
+ </div>
+ )
+ },
+ size: 250,
+ enableResizing: true,
+ meta: {
+ excelHeader: "문서명"
+ },
+ },
+
+ // 현재 스테이지
+ {
+ accessorKey: "currentStageName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="현재 스테이지" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ if (!doc.currentStageName) return <span className="text-gray-400">-</span>
+
+ return (
+ <div className="flex flex-col gap-1 items-start">
+ <span className="text-sm font-medium">{doc.currentStageName}</span>
+ <Badge
+ variant={getStatusColor(doc.currentStageStatus || '', doc.isOverdue || false)}
+ className="self-start inline-flex w-auto shrink-0 whitespace-nowrap text-xs"
+ >
+ {getStatusText(doc.currentStageStatus || '')}
+ </Badge>
+ </div>
+ )
+ },
+ size: 140,
+ enableResizing: true,
+ meta: {
+ excelHeader: "현재 스테이지"
+ },
+ },
+
+ // 일정 정보
+ {
+ accessorKey: "currentStagePlanDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="일정" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ if (!doc.currentStagePlanDate) return <span className="text-gray-400">-</span>
+
+ return (
+ <div className="flex flex-col gap-1">
+ <div className="text-sm">
+ <span className="text-gray-500">계획: </span>
+ <span>{formatDate(doc.currentStagePlanDate)}</span>
+ </div>
+ {doc.currentStageActualDate && (
+ <div className="text-sm">
+ <span className="text-gray-500">실제: </span>
+ <span>{formatDate(doc.currentStageActualDate)}</span>
+ </div>
+ )}
+ <DueDateInfo
+ daysUntilDue={doc.daysUntilDue}
+ isOverdue={doc.isOverdue || false}
+ />
+ </div>
+ )
+ },
+ size: 140,
+ enableResizing: true,
+ meta: {
+ excelHeader: "계획일"
+ },
+ },
+
+ // 진행률
+ {
+ accessorKey: "progressPercentage",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="진행률" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ const progress = doc.progressPercentage || 0
+ const completed = doc.completedStages || 0
+ const total = doc.totalStages || 0
+
+ return (
+ <div className="flex flex-col gap-2">
+ <div className="flex items-center gap-2">
+ <Progress value={progress} className="flex-1" />
+ <span className="text-sm font-medium text-gray-600 min-w-[3rem]">
+ {progress}%
+ </span>
+ </div>
+ <span className="text-xs text-gray-500">
+ {completed} / {total} 스테이지
+ </span>
+ </div>
+ )
+ },
+ size: 120,
+ enableResizing: true,
+ meta: {
+ excelHeader: "진행률"
+ },
+ },
+
+ // 최신 리비전
+ {
+ accessorKey: "latestRevision",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최신 리비전" />
+ ),
+ cell: ({ row }) => {
+ const doc = row.original
+ if (!doc.latestRevision) return <span className="text-gray-400">없음</span>
+
+ return (
+ <div className="flex flex-col gap-1 items-start">
+ <span className="font-mono text-sm font-medium">{doc.latestRevision}</span>
+ {/* <div className="text-xs text-gray-500">{doc.latestRevisionUploaderName}</div> */}
+ {doc.latestRevisionStatus && (
+ <Badge variant={getStatusColor(doc.latestRevisionStatus)} className="self-start inline-flex w-auto shrink-0 whitespace-nowrap text-xs" >
+ {getStatusText(doc.latestRevisionStatus)}
+ </Badge>
+ )}
+ {doc.latestSubmittedDate && (
+ <div className="text-xs text-gray-500">
+ {formatDate(doc.latestSubmittedDate)}
+ </div>
+ )}
+ </div>
+ )
+ },
+ size: 140,
+ enableResizing: true,
+ meta: {
+ excelHeader: "최신 리비전"
+ },
+ },
+
+ // 업데이트 일시
+ {
+ accessorKey: "updatedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="업데이트" />
+ ),
+ cell: ({ cell }) => (
+ <span className="text-sm text-gray-600">
+ {formatDateTime(cell.getValue() as Date)}
+ </span>
+ ),
+ size: 140,
+ enableResizing: true,
+ meta: {
+ excelHeader: "업데이트"
+ },
+ },
+
+ // 액션 메뉴
+ // 액션 메뉴
+ {
+ id: "actions",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ const doc = row.original
+ const canSubmit = doc.currentStageStatus === 'IN_PROGRESS'
+ const canApprove = doc.currentStageStatus === 'SUBMITTED'
+ const isPlantProject = projectType === "plant"
+
+ // 메뉴 아이템들을 그룹별로 정의
+ const viewActions = [
+ {
+ key: "view",
+ label: "상세보기",
+ icon: Eye,
+ action: () => setRowAction({ row, type: "view" }),
+ show: true
+ }
+ ]
+
+ const editActions = [
+ {
+ key: "update",
+ label: "편집",
+ icon: Edit,
+ action: () => setRowAction({ row, type: "update" }),
+ show: isPlantProject
+ }
+ ]
+
+ const fileActions = [
+ {
+ key: "upload",
+ label: "리비전 업로드",
+ icon: Upload,
+ action: () => setRowAction({ row, type: "upload" }),
+ show: canSubmit
+ }
+ ]
+
+ const dangerActions = [
+ {
+ key: "delete",
+ label: "삭제",
+ icon: Trash2,
+ action: () => setRowAction({ row, type: "delete" }),
+ show: isPlantProject,
+ className: "text-red-600",
+ shortcut: "⌘⌫"
+ }
+ ]
+
+ // 각 그룹에서 표시될 아이템이 있는지 확인
+ const hasEditActions = editActions.some(action => action.show)
+ const hasFileActions = fileActions.some(action => action.show)
+ const hasDangerActions = dangerActions.some(action => action.show)
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-7 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-48">
+ {/* 기본 액션 그룹 */}
+ {viewActions.map(action => action.show && (
+ <DropdownMenuItem
+ key={action.key}
+ onSelect={action.action}
+ className={action.className}
+ >
+ <action.icon className="mr-2 h-4 w-4" />
+ {action.label}
+ {action.shortcut && (
+ <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
+ )}
+ </DropdownMenuItem>
+ ))}
+
+ {/* 편집 액션 그룹 */}
+ {hasEditActions && (
+ <>
+ <DropdownMenuSeparator />
+ {editActions.map(action => action.show && (
+ <DropdownMenuItem
+ key={action.key}
+ onSelect={action.action}
+ className={action.className}
+ >
+ <action.icon className="mr-2 h-4 w-4" />
+ {action.label}
+ {action.shortcut && (
+ <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
+ )}
+ </DropdownMenuItem>
+ ))}
+ </>
+ )}
+
+ {/* 파일 액션 그룹 */}
+ {hasFileActions && (
+ <>
+ <DropdownMenuSeparator />
+ {fileActions.map(action => action.show && (
+ <DropdownMenuItem
+ key={action.key}
+ onSelect={action.action}
+ className={action.className}
+ >
+ <action.icon className="mr-2 h-4 w-4" />
+ {action.label}
+ {action.shortcut && (
+ <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
+ )}
+ </DropdownMenuItem>
+ ))}
+ </>
+ )}
+
+ {/* 위험한 액션 그룹 */}
+ {hasDangerActions && (
+ <>
+ <DropdownMenuSeparator />
+ {dangerActions.map(action => action.show && (
+ <DropdownMenuItem
+ key={action.key}
+ onSelect={action.action}
+ className={action.className}
+ >
+ <action.icon className="mr-2 h-4 w-4" />
+ {action.label}
+ {action.shortcut && (
+ <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
+ )}
+ </DropdownMenuItem>
+ ))}
+ </>
+ )}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ size: 40,
+ }
+ ]
+}
+
+// 확장된 행 컨텐츠 컴포넌트 (업데이트된 버전)
+export const UpdatedExpandedRowContent = ({
+ document
+}: {
+ document: EnhancedDocumentsView
+}) => {
+ if (!document.allStages || document.allStages.length === 0) {
+ return (
+ <div className="p-4 text-sm text-gray-500 italic">
+ 스테이지 정보가 없습니다.
+ </div>
+ )
+ }
+
+ return (
+ <div className="p-4 w-1/2">
+ <h4 className="font-medium mb-3 flex items-center gap-2">
+ <FileText className="w-4 h-4" />
+ 전체 스테이지 현황
+ </h4>
+
+ <div className="grid gap-3">
+ {document.allStages.map((stage, index) => (
+ <div key={stage.id} className="flex items-center justify-between p-3 bg-white rounded-lg border">
+ <div className="flex items-center gap-3">
+ <div className="flex items-center gap-2">
+ <div className="w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center text-xs font-medium">
+ {stage.stageOrder || index + 1}
+ </div>
+ <div className={cn(
+ "w-3 h-3 rounded-full",
+ stage.stageStatus === 'COMPLETED' ? 'bg-green-500' :
+ stage.stageStatus === 'IN_PROGRESS' ? 'bg-blue-500' :
+ stage.stageStatus === 'SUBMITTED' ? 'bg-purple-500' :
+ 'bg-gray-300'
+ )} />
+ </div>
+
+ <div>
+ <div className="font-medium text-sm">{stage.stageName}</div>
+ {stage.assigneeName && (
+ <div className="text-xs text-gray-500 flex items-center gap-1 mt-1">
+ <User className="w-3 h-3" />
+ {stage.assigneeName}
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div className="flex items-center gap-4 text-sm">
+ <div>
+ <span className="text-gray-500">계획: </span>
+ <span>{formatDate(stage.planDate)}</span>
+ </div>
+ {stage.actualDate && (
+ <div>
+ <span className="text-gray-500">완료: </span>
+ <span>{formatDate(stage.actualDate)}</span>
+ </div>
+ )}
+
+ <div className="flex items-center gap-2">
+ <Badge variant={getPriorityColor(stage.priority)} className="text-xs">
+ {getPriorityText(stage.priority)}
+ </Badge>
+ <Badge variant={getStatusColor(stage.stageStatus)} className="text-xs">
+ {getStatusText(stage.stageStatus)}
+ </Badge>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+} \ No newline at end of file