"use client"; import React, { useState, useMemo, useEffect, useCallback } 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 { Card, CardContent } from "@/components/ui/card"; 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, Download, Upload, Settings, RotateCcw, Plus, Save, BarChart3, ClipboardList, Inbox, Check, ChevronsUpDown, Loader2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; // --- Types --- type ClaimType = "불량" | "교환" | "반품" | "배송지연" | "기타"; type ClaimStatus = "접수" | "처리중" | "완료" | "취소"; interface Claim { claimNo: string; claimDate: string; claimType: ClaimType; claimStatus: ClaimStatus; customerCode: string; customerName: string; managerName: string; orderNo: string; claimContent: string; processContent: string; } interface CustomerOption { customerCode: string; customerName: string; } interface SalesOrderOption { orderNo: string; partnerName: string; status: string; } // --- Sample Data --- const initialData: Claim[] = [ { claimNo: "CLM-2025-004", claimDate: "2025-11-09", claimType: "불량", claimStatus: "접수", customerCode: "CUST-0001", customerName: "주식회사 코아스포트", managerName: "김철수", orderNo: "SO-2025-0102", claimContent: "제품 표면에 스크래치가 발견되었습니다.", processContent: "", }, { claimNo: "CLM-2025-001", claimDate: "2025-01-05", claimType: "불량", claimStatus: "접수", customerCode: "CUST-0002", customerName: "(주)현상산업", managerName: "김철수", orderNo: "SO-2025-0102", claimContent: "제품 불량", processContent: "", }, { claimNo: "CLM-2025-002", claimDate: "2025-01-04", claimType: "교환", claimStatus: "처리중", customerCode: "CUST-0003", customerName: "대한전섬", managerName: "이영희", orderNo: "SO-2025-0095", claimContent: "규격 불일치", processContent: "교환 진행 중", }, { claimNo: "CLM-2025-003", claimDate: "2025-01-03", claimType: "반품", claimStatus: "완료", customerCode: "CUST-0004", customerName: "삼성전자", managerName: "박민수", orderNo: "SO-2024-1285", claimContent: "수량 초과 납품", processContent: "반품 완료", }, ]; const getClaimTypeStyle = (type: ClaimType) => { switch (type) { 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-indigo-100 text-indigo-800 border-indigo-200"; case "기타": return "bg-gray-100 text-gray-800 border-gray-200"; default: return "bg-gray-100 text-gray-800 border-gray-200"; } }; const getClaimStatusStyle = (status: ClaimStatus) => { switch (status) { case "접수": return "bg-blue-100 text-blue-800 border-blue-200"; case "처리중": return "bg-amber-100 text-amber-800 border-amber-200"; case "완료": return "bg-emerald-100 text-emerald-800 border-emerald-200"; case "취소": return "bg-rose-100 text-rose-800 border-rose-200"; default: return "bg-gray-100 text-gray-800 border-gray-200"; } }; const CLAIM_TYPES: ClaimType[] = ["불량", "교환", "반품", "배송지연", "기타"]; const CLAIM_STATUSES: ClaimStatus[] = ["접수", "처리중", "완료", "취소"]; export default function ClaimManagementPage() { const [data, setData] = useState(initialData); const [selectedClaimNo, setSelectedClaimNo] = useState(null); // 검색 상태 const [searchDateFrom, setSearchDateFrom] = useState(""); const [searchDateTo, setSearchDateTo] = useState(""); const [searchClaimType, setSearchClaimType] = useState("all"); const [searchStatus, setSearchStatus] = useState("all"); const [searchCustomer, setSearchCustomer] = useState(""); const [searchClaimNo, setSearchClaimNo] = useState(""); // 모달 상태 const [isModalOpen, setIsModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [formData, setFormData] = useState>({}); // Combobox 상태 const [customerOpen, setCustomerOpen] = useState(false); const [orderOpen, setOrderOpen] = useState(false); // DB 데이터 const [customers, setCustomers] = useState([]); const [salesOrders, setSalesOrders] = useState([]); const [customersLoading, setCustomersLoading] = useState(false); const [ordersLoading, setOrdersLoading] = useState(false); useEffect(() => { const today = new Date(); const thirtyDaysAgo = new Date(today); thirtyDaysAgo.setDate(today.getDate() - 30); setSearchDateFrom(thirtyDaysAgo.toISOString().split("T")[0]); setSearchDateTo(today.toISOString().split("T")[0]); }, []); // 거래처 목록 조회 const fetchCustomers = useCallback(async () => { if (customers.length > 0) return; setCustomersLoading(true); try { const res = await apiClient.post("/table-management/tables/customer_mng/data", { page: 1, size: 9999, autoFilter: true, }); if (res.data?.success && res.data?.data?.rows) { const list: CustomerOption[] = res.data.data.rows.map((row: any) => ({ customerCode: row.customer_code || "", customerName: row.customer_name || "", })); setCustomers(list); } } catch (e) { console.error("거래처 목록 조회 실패:", e); } finally { setCustomersLoading(false); } }, [customers.length]); // 수주 목록 조회 const fetchSalesOrders = useCallback(async () => { if (salesOrders.length > 0) return; setOrdersLoading(true); try { const res = await apiClient.post("/table-management/tables/sales_order_mng/data", { page: 1, size: 9999, autoFilter: true, }); if (res.data?.success && res.data?.data?.rows) { const seen = new Set(); const list: SalesOrderOption[] = []; for (const row of res.data.data.rows) { const orderNo = row.order_no || ""; if (!orderNo || seen.has(orderNo)) continue; seen.add(orderNo); list.push({ orderNo, partnerName: row.partner_id || "", status: row.status || "", }); } setSalesOrders(list); } } catch (e) { console.error("수주 목록 조회 실패:", e); } finally { setOrdersLoading(false); } }, [salesOrders.length]); const filteredData = useMemo(() => { return data .filter((claim) => { if (searchDateFrom && claim.claimDate < searchDateFrom) return false; if (searchDateTo && claim.claimDate > searchDateTo) return false; if (searchClaimType !== "all" && claim.claimType !== searchClaimType) return false; if (searchStatus !== "all" && claim.claimStatus !== searchStatus) return false; if ( searchCustomer && !claim.customerName .toLowerCase() .includes(searchCustomer.toLowerCase()) ) return false; if ( searchClaimNo && !claim.claimNo.toLowerCase().includes(searchClaimNo.toLowerCase()) ) return false; return true; }) .sort((a, b) => b.claimDate.localeCompare(a.claimDate)); }, [ data, searchDateFrom, searchDateTo, searchClaimType, searchStatus, searchCustomer, searchClaimNo, ]); // 상태별 카운트 const statusCounts = useMemo(() => { const counts = { 접수: 0, 처리중: 0, 완료: 0, 취소: 0 }; data.forEach((claim) => { if (counts[claim.claimStatus] !== undefined) { counts[claim.claimStatus]++; } }); return counts; }, [data]); const generateClaimNo = useCallback(() => { const year = new Date().getFullYear(); const prefix = `CLM-${year}-`; const existingNumbers = data .filter((c) => c.claimNo.startsWith(prefix)) .map((c) => parseInt(c.claimNo.replace(prefix, ""), 10)) .filter((n) => !isNaN(n)); const maxNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) : 0; return `${prefix}${String(maxNumber + 1).padStart(3, "0")}`; }, [data]); const handleRowClick = (claimNo: string) => { setSelectedClaimNo(claimNo); }; const openRegisterModal = () => { setIsEditMode(false); setFormData({ claimNo: generateClaimNo(), claimDate: new Date().toISOString().split("T")[0], claimType: undefined, claimStatus: "접수", customerCode: "", customerName: "", managerName: "", orderNo: "", claimContent: "", processContent: "", }); setIsModalOpen(true); }; const openEditModal = (claimNo: string) => { const claim = data.find((c) => c.claimNo === claimNo); if (!claim) return; setIsEditMode(true); setFormData({ ...claim }); setIsModalOpen(true); }; const handleFormChange = (field: keyof Claim, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; const handleSave = () => { if (!formData.claimType || !formData.customerName || !formData.claimContent) { alert("필수 항목을 모두 입력해주세요."); return; } const claimData: Claim = { claimNo: formData.claimNo || "", claimDate: formData.claimDate || new Date().toISOString().split("T")[0], claimType: formData.claimType as ClaimType, claimStatus: (formData.claimStatus as ClaimStatus) || "접수", customerCode: formData.customerCode || "", customerName: formData.customerName || "", managerName: formData.managerName || "", orderNo: formData.orderNo || "", claimContent: formData.claimContent || "", processContent: formData.processContent || "", }; if (isEditMode) { setData((prev) => prev.map((c) => (c.claimNo === claimData.claimNo ? claimData : c)) ); } else { setData((prev) => [claimData, ...prev]); } setIsModalOpen(false); alert("클레임이 저장되었습니다."); }; const handleResetSearch = () => { const today = new Date(); const thirtyDaysAgo = new Date(today); thirtyDaysAgo.setDate(today.getDate() - 30); setSearchDateFrom(thirtyDaysAgo.toISOString().split("T")[0]); setSearchDateTo(today.toISOString().split("T")[0]); setSearchClaimType("all"); setSearchStatus("all"); setSearchCustomer(""); setSearchClaimNo(""); }; const selectedClaim = useMemo( () => data.find((c) => c.claimNo === selectedClaimNo), [data, selectedClaimNo] ); const statCards: { label: string; value: number; gradient: string; textColor: string; }[] = [ { label: "접수", value: statusCounts["접수"], gradient: "from-indigo-500 to-purple-600", textColor: "text-white", }, { label: "처리중", value: statusCounts["처리중"], gradient: "from-amber-300 to-amber-500", textColor: "text-amber-900", }, { label: "완료", value: statusCounts["완료"], gradient: "from-cyan-400 to-blue-500", textColor: "text-white", }, { label: "취소", value: statusCounts["취소"], gradient: "from-slate-300 to-slate-400", textColor: "text-slate-800", }, ]; return (
{/* 검색 섹션 */}
setSearchDateFrom(e.target.value)} /> ~ setSearchDateTo(e.target.value)} />
setSearchCustomer(e.target.value)} />
setSearchClaimNo(e.target.value)} />
{/* 메인 분할 레이아웃 */}
{/* 왼쪽: 클레임 목록 */}
클레임 목록 {filteredData.length}건
No 클레임번호 접수일자 유형 상태 거래처명 담당자 수주번호 클레임 내용 {filteredData.length === 0 ? (
등록된 클레임이 없습니다
) : ( filteredData.map((claim, idx) => ( handleRowClick(claim.claimNo)} onDoubleClick={() => openEditModal(claim.claimNo)} > {idx + 1} {claim.claimNo} {claim.claimDate} {claim.claimType} {claim.claimStatus} {claim.customerName} {claim.managerName || "-"} {claim.orderNo || "-"} {claim.claimContent || "-"} )) )}
{/* 오른쪽: 처리 현황 */}
처리 현황
{/* 상태별 카드 */}
{statCards.map((card) => (
{card.label}
{card.value}
))}
{/* 선택된 클레임 상세 */} {selectedClaim ? (

클레임 상세 - {selectedClaim.claimNo}

클레임번호 {selectedClaim.claimNo}
접수일자 {selectedClaim.claimDate}
유형 {selectedClaim.claimType}
상태 {selectedClaim.claimStatus}
거래처명 {selectedClaim.customerName}
담당자 {selectedClaim.managerName || "-"}
수주번호 {selectedClaim.orderNo || "-"}
클레임 내용
{selectedClaim.claimContent || "-"}
처리 내용
{selectedClaim.processContent || "-"}
) : (

클레임을 선택하면 상세 정보가 표시됩니다

)}
{/* 클레임 등록/수정 모달 */} {isEditMode ? "클레임 수정" : "클레임 등록"} {isEditMode ? "클레임 정보를 수정합니다." : "새로운 클레임을 등록합니다."}
{/* 왼쪽: 기본 정보 */}

클레임 기본 정보

handleFormChange("claimDate", e.target.value) } className="h-8 text-xs sm:h-10 sm:text-sm" />
{customersLoading ? (
로딩 중...
) : ( <> 거래처를 찾을 수 없습니다. {customers.map((cust) => ( { setFormData((prev) => ({ ...prev, customerCode: cust.customerCode, customerName: cust.customerName, })); setCustomerOpen(false); }} className="text-xs sm:text-sm" >
{cust.customerName} {cust.customerCode}
))}
)}
handleFormChange("managerName", e.target.value) } className="h-8 text-xs sm:h-10 sm:text-sm" />
{ordersLoading ? (
로딩 중...
) : ( <> 수주번호를 찾을 수 없습니다. {salesOrders.map((order) => ( { setFormData((prev) => ({ ...prev, orderNo: order.orderNo, })); setOrderOpen(false); }} className="text-xs sm:text-sm" >
{order.orderNo} {order.status} {order.partnerName ? ` | ${order.partnerName}` : ""}
))}
)}
{/* 오른쪽: 상세 내용 */}

클레임 상세 내용