"use client"; import React, { useState, useMemo, useCallback, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Search, RotateCcw, RefreshCw, ClipboardList, Pencil, Inbox, CheckCircle2, XCircle, Rocket, Eye, ChevronRight, ArrowRight, Check, ChevronsUpDown, UserCircle, Loader2, User, Users, } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { getDesignRequestList, updateDesignRequest, addRequestHistory, createProject, } from "@/lib/api/design"; import { getUserList } from "@/lib/api/user"; import { useAuth } from "@/hooks/useAuth"; // --- Types --- type SourceType = "dr" | "ecr"; type TaskStatus = "신규접수" | "검토중" | "승인완료" | "반려" | "프로젝트생성"; type Priority = "긴급" | "높음" | "보통" | "낮음"; type MainTab = "all" | "dr" | "ecr"; interface HistoryItem { step: string; date: string; user: string; desc: string; } interface TaskItem { dbId: string; id: string; sourceType: SourceType; date: string; dueDate: string; priority: Priority; status: TaskStatus; approvalStep: number; targetName: string; customer: string; reqDept: string; requester: string; designer: string; orderNo?: string; designType?: string; spec?: string; changeType?: string; drawingNo?: string; reason?: string; impact?: string[]; reviewMemo: string; projectNo?: string; history: HistoryItem[]; } // --- 상태/우선순위 배지 색상 --- const getStatusVariant = (status: TaskStatus) => { switch (status) { case "신규접수": return "bg-amber-100 text-amber-800 border-amber-200"; case "검토중": return "bg-blue-100 text-blue-800 border-blue-200"; case "승인완료": return "bg-emerald-100 text-emerald-800 border-emerald-200"; case "반려": return "bg-rose-100 text-rose-800 border-rose-200"; case "프로젝트생성": return "bg-violet-100 text-violet-800 border-violet-200"; default: return "bg-gray-100 text-gray-800 border-gray-200"; } }; const getPriorityVariant = (priority: Priority) => { switch (priority) { case "긴급": return "bg-rose-100 text-rose-800 border-rose-200"; case "높음": return "bg-amber-100 text-amber-800 border-amber-200"; case "보통": return "bg-blue-100 text-blue-800 border-blue-200"; case "낮음": return "bg-gray-100 text-gray-600 border-gray-200"; default: return "bg-gray-100 text-gray-600 border-gray-200"; } }; const getSourceBadge = (type: SourceType) => { if (type === "dr") return "bg-blue-100 text-blue-800 border-blue-200"; return "bg-amber-100 text-amber-800 border-amber-200"; }; const getStepIcon = (step: string) => { switch (step) { case "접수": return ; case "검토": return ; case "승인": return ; case "반려": return ; case "프로젝트": return ; default: return ; } }; // --- API 응답 → TaskItem 매핑 --- function mapApiToTaskItem(r: any): TaskItem { const historyRaw = Array.isArray(r.history) ? r.history : []; const history: HistoryItem[] = historyRaw.map((h: any) => ({ step: h.step || "", date: h.history_date || "", user: h.user_name || "", desc: h.description || "", })); const impact = Array.isArray(r.impact) ? r.impact : []; const formatDate = (d: string | null | undefined) => d ? (d.includes("T") ? d.split("T")[0] : d) : ""; return { dbId: r.id, id: r.request_no || r.id, sourceType: (r.source_type === "ecr" ? "ecr" : "dr") as SourceType, date: formatDate(r.request_date), dueDate: formatDate(r.due_date), priority: (r.priority || "보통") as Priority, status: (r.status || "신규접수") as TaskStatus, approvalStep: typeof r.approval_step === "number" ? r.approval_step : 0, targetName: r.target_name || "", customer: r.customer || "", reqDept: r.req_dept || "", requester: r.requester || "", designer: r.designer || "", orderNo: r.order_no, designType: r.design_type, spec: r.spec, changeType: r.change_type, drawingNo: r.drawing_no, reason: r.reason, impact: impact.length ? impact : undefined, reviewMemo: r.review_memo || "", projectNo: r.project_id, history, }; } // --- 승인 프로세스 스텝 --- const APPROVAL_STEPS = [ { label: "접수", step: 0 }, { label: "검토", step: 1 }, { label: "내부승인", step: 2 }, { label: "프로젝트", step: 3 }, ]; // --- 현황 카드 설정 --- const STAT_CARDS: { label: string; status: TaskStatus; color: string; textColor: string }[] = [ { label: "신규접수", status: "신규접수", color: "from-indigo-500 to-purple-600", textColor: "text-white" }, { label: "검토중", status: "검토중", color: "from-amber-400 to-orange-500", textColor: "text-gray-900" }, { label: "승인완료", status: "승인완료", color: "from-cyan-400 to-blue-500", textColor: "text-white" }, { label: "반려", status: "반려", color: "from-rose-400 to-red-500", textColor: "text-white" }, { label: "프로젝트", status: "프로젝트생성", color: "from-violet-400 to-purple-500", textColor: "text-white" }, ]; interface EmployeeOption { userId: string; userName: string; deptName: string; } export default function DesignTaskManagementPage() { const { user, userName, loading: authLoading } = useAuth(); const [allTasks, setAllTasks] = useState([]); const [loading, setLoading] = useState(true); const [selectedTaskId, setSelectedTaskId] = useState(null); const [currentTab, setCurrentTab] = useState("all"); const [employees, setEmployees] = useState([]); const [myTasksOnly, setMyTasksOnly] = useState(true); const fetchEmployees = useCallback(async () => { try { const res = await getUserList({ size: 1000 }); if (res.success && res.data) { const list = (res.data as any[]).map((u: any) => ({ userId: u.user_id || u.userId, userName: u.user_name || u.userName || "", deptName: u.dept_name || u.deptName || "", })); setEmployees(list); } } catch { // 사원 목록 로드 실패 시 빈 배열 유지 } }, []); const fetchTasks = useCallback(async () => { setLoading(true); const res = await getDesignRequestList({}); setLoading(false); if (res.success && res.data) { const mapped = (res.data as any[]).map(mapApiToTaskItem); setAllTasks(mapped); } else { toast.error(res.message || "데이터를 불러오지 못했습니다."); setAllTasks([]); } }, []); useEffect(() => { fetchTasks(); fetchEmployees(); }, [fetchTasks, fetchEmployees]); // 검색 필터 const [searchStatus, setSearchStatus] = useState("all"); const [searchPriority, setSearchPriority] = useState("all"); const [searchReqDept, setSearchReqDept] = useState("all"); const [searchKeyword, setSearchKeyword] = useState(""); // 담당자 선택 모달 상태 const [designerModalOpen, setDesignerModalOpen] = useState(false); const [designerModalTaskId, setDesignerModalTaskId] = useState(null); const [designerModalValue, setDesignerModalValue] = useState(""); const [designerComboOpen, setDesignerComboOpen] = useState(false); // 모달 상태 const [rejectModalOpen, setRejectModalOpen] = useState(false); const [rejectTaskId, setRejectTaskId] = useState(null); const [rejectReason, setRejectReason] = useState(""); const [projectModalOpen, setProjectModalOpen] = useState(false); const [projectTaskId, setProjectTaskId] = useState(null); const [pmComboOpen, setPmComboOpen] = useState(false); const [projectForm, setProjectForm] = useState({ projNo: "", projName: "", projSourceNo: "", projStartDate: "", projEndDate: "", projPM: "", projCustomer: "", projDesc: "", }); // 검토 메모 const [reviewMemoText, setReviewMemoText] = useState(""); // 현재 사용자 관련 업무만 필터링 const myRelatedTasks = useMemo(() => { if (!myTasksOnly || !userName) return allTasks; const currentUserName = userName; const currentDeptName = user?.deptName || ""; return allTasks.filter((item) => { if (item.requester === currentUserName) return true; if (item.designer === currentUserName) return true; if (currentDeptName && item.reqDept === currentDeptName) return true; const inHistory = item.history.some((h) => h.user === currentUserName); if (inHistory) return true; return false; }); }, [allTasks, myTasksOnly, userName, user?.deptName]); // 탭별 카운트 const tabCounts = useMemo(() => { const drItems = myRelatedTasks.filter((t) => t.sourceType === "dr"); const ecrItems = myRelatedTasks.filter((t) => t.sourceType === "ecr"); const newDR = drItems.filter((t) => t.status === "신규접수").length; const newECR = ecrItems.filter((t) => t.status === "신규접수").length; return { all: newDR + newECR || myRelatedTasks.length, allIsNew: newDR + newECR > 0, dr: newDR || drItems.length, drIsNew: newDR > 0, ecr: newECR || ecrItems.length, ecrIsNew: newECR > 0, }; }, [myRelatedTasks]); // 필터링된 데이터 const filteredData = useMemo(() => { return myRelatedTasks.filter((item) => { if (currentTab === "dr" && item.sourceType !== "dr") return false; if (currentTab === "ecr" && item.sourceType !== "ecr") return false; if (searchStatus !== "all" && item.status !== searchStatus) return false; if (searchPriority !== "all" && item.priority !== searchPriority) return false; if (searchReqDept !== "all" && item.reqDept !== searchReqDept) return false; if (searchKeyword) { const str = [item.id, item.targetName, item.customer, item.requester, item.designer, item.reqDept] .join(" ") .toLowerCase(); if (!str.includes(searchKeyword.toLowerCase())) return false; } return true; }); }, [myRelatedTasks, currentTab, searchStatus, searchPriority, searchReqDept, searchKeyword]); // 현황 통계 const stats = useMemo(() => { return { 신규접수: myRelatedTasks.filter((t) => t.status === "신규접수").length, 검토중: myRelatedTasks.filter((t) => t.status === "검토중").length, 승인완료: myRelatedTasks.filter((t) => t.status === "승인완료").length, 반려: myRelatedTasks.filter((t) => t.status === "반려").length, 프로젝트생성: myRelatedTasks.filter((t) => t.status === "프로젝트생성").length, }; }, [myRelatedTasks]); const selectedTask = useMemo( () => allTasks.find((t) => t.dbId === selectedTaskId) || null, [allTasks, selectedTaskId] ); // 선택된 업무 변경 시 메모 동기화 useEffect(() => { if (selectedTask) { setReviewMemoText(selectedTask.reviewMemo || ""); } }, [selectedTask]); // --- 액션 핸들러 --- const handleSelectTask = useCallback((dbId: string) => { setSelectedTaskId(dbId); }, []); const handleOpenDesignerModal = useCallback((dbId: string) => { setDesignerModalTaskId(dbId); setDesignerModalValue(""); setDesignerComboOpen(false); setDesignerModalOpen(true); }, []); const handleConfirmDesigner = useCallback(async () => { if (!designerModalValue) { toast.error("설계 담당자를 선택하세요."); return; } if (!designerModalTaskId) return; const selected = employees.find((e) => e.userId === designerModalValue); const designerName = selected?.userName || designerModalValue; const historyDate = new Date().toISOString().split("T")[0]; const historyRes = await addRequestHistory(designerModalTaskId, { step: "검토", history_date: historyDate, user_name: designerName, description: "검토 착수 - 담당자 배정", }); if (!historyRes.success) { toast.error(historyRes.message || "이력 추가에 실패했습니다."); return; } const updateRes = await updateDesignRequest(designerModalTaskId, { status: "검토중", approval_step: 1, designer: designerName, }); if (!updateRes.success) { toast.error(updateRes.message || "상태 업데이트에 실패했습니다."); return; } setDesignerModalOpen(false); toast.success("검토가 착수되었습니다."); fetchTasks(); }, [designerModalTaskId, designerModalValue, employees, fetchTasks]); const handleApprove = useCallback( async (dbId: string) => { if (!confirm("내부 승인을 진행하시겠습니까?")) return; const historyDate = new Date().toISOString().split("T")[0]; const historyRes = await addRequestHistory(dbId, { step: "승인", history_date: historyDate, user_name: "팀장", description: "내부 검토 완료, 승인 처리", }); if (!historyRes.success) { toast.error(historyRes.message || "이력 추가에 실패했습니다."); return; } const updateRes = await updateDesignRequest(dbId, { status: "승인완료", approval_step: 3, }); if (!updateRes.success) { toast.error(updateRes.message || "상태 업데이트에 실패했습니다."); return; } toast.success("승인 처리되었습니다."); fetchTasks(); }, [fetchTasks] ); const handleOpenRejectModal = useCallback((dbId: string) => { setRejectTaskId(dbId); setRejectReason(""); setRejectModalOpen(true); }, []); const handleConfirmReject = useCallback(async () => { if (!rejectReason.trim()) { toast.error("반려 사유를 입력하세요."); return; } if (!rejectTaskId) return; const historyDate = new Date().toISOString().split("T")[0]; const historyRes = await addRequestHistory(rejectTaskId, { step: "반려", history_date: historyDate, user_name: "팀장", description: rejectReason, }); if (!historyRes.success) { toast.error(historyRes.message || "이력 추가에 실패했습니다."); return; } const updateRes = await updateDesignRequest(rejectTaskId, { status: "반려", approval_step: -1, review_memo: rejectReason, }); if (!updateRes.success) { toast.error(updateRes.message || "상태 업데이트에 실패했습니다."); return; } setRejectModalOpen(false); toast.success("반려 처리되었습니다."); fetchTasks(); }, [rejectTaskId, rejectReason, fetchTasks]); const handleOpenProjectModal = useCallback( (dbId: string) => { const task = allTasks.find((t) => t.dbId === dbId); if (!task) return; const year = new Date().getFullYear(); const existingProjects = allTasks.filter((t) => t.projectNo).length; const projNo = `PJ-${year}-${String(existingProjects + 1).padStart(4, "0")}`; setProjectTaskId(dbId); const matchedEmployee = employees.find((e) => e.userName === task.designer); setProjectForm({ projNo, projName: task.targetName, projSourceNo: task.id, projStartDate: new Date().toISOString().split("T")[0], projEndDate: task.dueDate, projPM: matchedEmployee?.userId || "", projCustomer: task.customer || task.reqDept, projDesc: task.sourceType === "dr" ? task.spec || "" : task.reason || "", }); setProjectModalOpen(true); }, [allTasks, employees] ); const handleCreateProject = useCallback(async () => { if (!projectForm.projName.trim()) { toast.error("프로젝트명을 입력하세요."); return; } if (!projectForm.projStartDate) { toast.error("시작일을 입력하세요."); return; } if (!projectForm.projEndDate) { toast.error("종료예정일을 입력하세요."); return; } if (!projectForm.projPM) { toast.error("PM을 선택하세요."); return; } if (!projectTaskId) return; const pmEmployee = employees.find((e) => e.userId === projectForm.projPM); const pmName = pmEmployee?.userName || projectForm.projPM; // 1) 실제 프로젝트 테이블(dsn_project)에 INSERT const projectRes = await createProject({ project_no: projectForm.projNo, name: projectForm.projName, status: "계획", pm: pmName, customer: projectForm.projCustomer, start_date: projectForm.projStartDate, end_date: projectForm.projEndDate, source_no: projectForm.projSourceNo, description: projectForm.projDesc, progress: "0", }); if (!projectRes.success) { toast.error(projectRes.message || "프로젝트 생성에 실패했습니다."); return; } const createdProjectId = projectRes.data?.id || projectForm.projNo; // 2) 이력 추가 const historyDate = new Date().toISOString().split("T")[0]; const historyRes = await addRequestHistory(projectTaskId, { step: "프로젝트", history_date: historyDate, user_name: pmName, description: `${projectForm.projNo} 프로젝트 생성 - ${projectForm.projName}`, }); if (!historyRes.success) { toast.error(historyRes.message || "이력 추가에 실패했습니다."); return; } // 3) 설계요청 상태 업데이트 + 프로젝트 ID 연결 const updateRes = await updateDesignRequest(projectTaskId, { status: "프로젝트생성", approval_step: 4, project_id: createdProjectId, }); if (!updateRes.success) { toast.error(updateRes.message || "상태 업데이트에 실패했습니다."); return; } setProjectModalOpen(false); toast.success(`프로젝트 ${projectForm.projNo}가 생성되었습니다.`); fetchTasks(); }, [projectForm, projectTaskId, employees, fetchTasks]); const handleSaveReviewMemo = useCallback(async () => { if (!selectedTaskId) return; const updateRes = await updateDesignRequest(selectedTaskId, { review_memo: reviewMemoText, }); if (!updateRes.success) { toast.error(updateRes.message || "메모 저장에 실패했습니다."); return; } toast.success("검토 메모가 저장되었습니다."); fetchTasks(); }, [selectedTaskId, reviewMemoText, fetchTasks]); const handleResetSearch = useCallback(() => { setSearchStatus("all"); setSearchPriority("all"); setSearchReqDept("all"); setSearchKeyword(""); }, []); const handleFilterByStatus = useCallback((status: TaskStatus) => { setSearchStatus(status); }, []); // 납기 남은 일수 계산 const getDueDateInfo = (dueDate: string) => { const due = new Date(dueDate); const today = new Date(); const diffDays = Math.ceil((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); const color = diffDays < 0 ? "text-rose-600" : diffDays <= 7 ? "text-amber-600" : "text-emerald-600"; const text = diffDays < 0 ? `${Math.abs(diffDays)}일 초과` : diffDays === 0 ? "오늘" : `${diffDays}일 남음`; return { color, text }; }; return (
{/* 탭 바 */}
{([ { key: "all" as MainTab, label: "전체", icon: , count: tabCounts.all, isNew: tabCounts.allIsNew }, { key: "dr" as MainTab, label: "설계의뢰(DR)", icon: , count: tabCounts.dr, isNew: tabCounts.drIsNew }, { key: "ecr" as MainTab, label: "설계변경(ECR)", icon: , count: tabCounts.ecr, isNew: tabCounts.ecrIsNew }, ]).map((tab) => ( ))}
{userName && (
{userName} {user?.deptName && ({user.deptName})}
)}
실시간 동기화 중
{/* 검색 섹션 */}
setSearchKeyword(e.target.value)} placeholder="접수번호 / 설비명 / 품목명 / 고객명 검색" className="h-9 pl-9 text-xs" />
{/* 좌우 분할 패널 */}
{/* 왼쪽: 접수 목록 */}

{myTasksOnly ? "내 관련 업무" : "접수 업무 목록"} ({filteredData.length}건) {myTasksOnly && ( 전체 {allTasks.length}건 중 )}

구분 접수번호 상태 우선순위 설비/품목명 의뢰부서 의뢰자 접수일자 희망납기 설계담당 {loading && allTasks.length === 0 ? (
로딩 중...
) : filteredData.length === 0 ? (
조건에 맞는 업무가 없습니다
) : ( filteredData.map((item) => ( handleSelectTask(item.dbId)} > {item.sourceType === "dr" ? "DR" : "ECR"} {item.id} {item.status} {item.priority} {item.targetName} {item.reqDept} {item.requester} {item.date} {item.dueDate} {item.designer || 미배정} )) )}
{/* 오른쪽: 검토 및 승인 */}

검토 및 승인

{/* 현황 카드 */}
{STAT_CARDS.map((card) => ( ))}
{/* 상세 정보 */} {!selectedTask ? (
좌측 목록에서 업무를 선택하세요
) : (
{/* 승인 프로세스 */}

승인 프로세스

{APPROVAL_STEPS.map((s, idx) => { let stepClass = "border-border bg-card text-muted-foreground"; if (selectedTask.status === "반려" && idx >= 2) { stepClass = idx === 2 ? "border-rose-400 bg-rose-50 text-rose-800 dark:bg-rose-950/30 dark:text-rose-300" : "border-border bg-card text-muted-foreground"; } else if (selectedTask.approvalStep > idx) { stepClass = "border-emerald-400 bg-emerald-50 text-emerald-800 dark:bg-emerald-950/30 dark:text-emerald-300"; } else if (selectedTask.approvalStep === idx) { stepClass = "border-primary bg-primary/5 text-primary ring-2 ring-primary/10"; } return (
{s.label}
{idx < APPROVAL_STEPS.length - 1 && ( )}
); })}
{/* 액션 버튼 */}
{selectedTask.status === "신규접수" && ( <> )} {selectedTask.status === "검토중" && ( <> )} {selectedTask.status === "승인완료" && ( )} {selectedTask.status === "프로젝트생성" && selectedTask.projectNo && ( )}
{/* 접수 정보 */}

접수 정보

{selectedTask.sourceType === "dr" ? ( <> ) : ( <> )}
접수번호 {selectedTask.id} 구분 {selectedTask.sourceType === "dr" ? "설계의뢰(DR)" : "설계변경(ECR)"}
상태 {selectedTask.status} 우선순위 {selectedTask.priority}
{selectedTask.sourceType === "dr" ? "설비/품목명" : "품목명"} {selectedTask.targetName}
의뢰부서 {selectedTask.reqDept} 의뢰자 {selectedTask.requester}
설계유형 {selectedTask.designType || "-"} 고객명 {selectedTask.customer || "-"}
수주번호 {selectedTask.orderNo || "-"} 설계담당 {selectedTask.designer || 미배정}
변경유형 {selectedTask.changeType || "-"} 도면번호 {selectedTask.drawingNo || "-"}
설계담당 {selectedTask.designer || 미배정} 영향범위 {selectedTask.impact ? (
{selectedTask.impact.map((i) => ( {i} ))}
) : "-"}
접수일자 {selectedTask.date} 희망납기 {selectedTask.dueDate}{" "} ({getDueDateInfo(selectedTask.dueDate).text})
{/* 요구사양 / 변경사유 */}

{selectedTask.sourceType === "dr" ? "요구사양" : "변경 사유"}

                        {selectedTask.sourceType === "dr" ? selectedTask.spec : selectedTask.reason}
                      
{/* 검토 메모 */}

검토 의견 / 메모