"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 { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Search, Download, X, Save, Ban, RotateCcw, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { getShipmentPlanList, updateShipmentPlan, type ShipmentPlanListItem, } from "@/lib/api/shipping"; const STATUS_OPTIONS = [ { value: "all", label: "전체" }, { value: "READY", label: "준비" }, { value: "CONFIRMED", label: "확정" }, { value: "SHIPPING", label: "출하중" }, { value: "COMPLETED", label: "완료" }, { value: "CANCEL_REQUEST", label: "취소요청" }, { value: "CANCELLED", label: "취소완료" }, ]; const getStatusLabel = (status: string) => { const found = STATUS_OPTIONS.find(o => o.value === status); return found?.label || status; }; const getStatusColor = (status: string) => { switch (status) { case "READY": return "bg-blue-100 text-blue-800 border-blue-200"; case "CONFIRMED": return "bg-indigo-100 text-indigo-800 border-indigo-200"; case "SHIPPING": return "bg-amber-100 text-amber-800 border-amber-200"; case "COMPLETED": return "bg-emerald-100 text-emerald-800 border-emerald-200"; case "CANCEL_REQUEST": return "bg-rose-100 text-rose-800 border-rose-200"; case "CANCELLED": return "bg-slate-100 text-slate-800 border-slate-200"; default: return "bg-gray-100 text-gray-800 border-gray-200"; } }; export default function ShippingPlanPage() { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [selectedId, setSelectedId] = useState(null); const [checkedIds, setCheckedIds] = useState([]); // 검색 const [searchDateFrom, setSearchDateFrom] = useState(""); const [searchDateTo, setSearchDateTo] = useState(""); const [searchStatus, setSearchStatus] = useState("all"); const [searchCustomer, setSearchCustomer] = useState(""); const [searchKeyword, setSearchKeyword] = useState(""); // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); const [editPlanDate, setEditPlanDate] = useState(""); const [editMemo, setEditMemo] = useState(""); const [isDetailChanged, setIsDetailChanged] = useState(false); const [saving, setSaving] = useState(false); // 날짜 초기화 useEffect(() => { const today = new Date(); const threeMonthsAgo = new Date(today); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); const oneMonthLater = new Date(today); oneMonthLater.setMonth(oneMonthLater.getMonth() + 1); setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); setSearchDateTo(oneMonthLater.toISOString().split("T")[0]); }, []); // 데이터 조회 const fetchData = useCallback(async () => { setLoading(true); try { const params: any = {}; if (searchDateFrom) params.dateFrom = searchDateFrom; if (searchDateTo) params.dateTo = searchDateTo; if (searchStatus !== "all") params.status = searchStatus; if (searchCustomer.trim()) params.customer = searchCustomer.trim(); if (searchKeyword.trim()) params.keyword = searchKeyword.trim(); const result = await getShipmentPlanList(params); if (result.success) { setData(result.data || []); } } catch (err) { console.error("출하계획 조회 실패:", err); } finally { setLoading(false); } }, [searchDateFrom, searchDateTo, searchStatus, searchCustomer, searchKeyword]); // 초기 로드 + 검색 시 자동 조회 useEffect(() => { if (searchDateFrom && searchDateTo) { fetchData(); } }, [searchDateFrom, searchDateTo]); const handleSearch = () => fetchData(); const handleResetSearch = () => { setSearchStatus("all"); setSearchCustomer(""); setSearchKeyword(""); const today = new Date(); const threeMonthsAgo = new Date(today); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); const oneMonthLater = new Date(today); oneMonthLater.setMonth(oneMonthLater.getMonth() + 1); setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); setSearchDateTo(oneMonthLater.toISOString().split("T")[0]); }; const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const handleRowClick = (plan: ShipmentPlanListItem) => { if (isDetailChanged && selectedId !== plan.id) { if (!confirm("변경사항이 있습니다. 저장하지 않고 이동하시겠습니까?")) return; } setSelectedId(plan.id); setEditPlanQty(String(Number(plan.plan_qty))); setEditPlanDate(plan.plan_date ? plan.plan_date.split("T")[0] : ""); setEditMemo(plan.memo || ""); setIsDetailChanged(false); }; const handleCheckAll = (checked: boolean) => { if (checked) { setCheckedIds(data.filter(p => p.status !== "CANCELLED").map(p => p.id)); } else { setCheckedIds([]); } }; const handleCheck = (id: number, checked: boolean) => { if (checked) { setCheckedIds(prev => [...prev, id]); } else { setCheckedIds(prev => prev.filter(i => i !== id)); } }; const handleSaveDetail = async () => { if (!selectedId || !selectedPlan) return; const qty = Number(editPlanQty); if (qty <= 0) { alert("계획수량은 0보다 커야 합니다."); return; } if (!editPlanDate) { alert("출하계획일을 입력해주세요."); return; } setSaving(true); try { const result = await updateShipmentPlan(selectedId, { planQty: qty, planDate: editPlanDate, memo: editMemo, }); if (result.success) { setIsDetailChanged(false); alert("저장되었습니다."); fetchData(); } else { alert(result.message || "저장 실패"); } } catch (err: any) { alert(err.message || "저장 중 오류 발생"); } finally { setSaving(false); } }; const formatDate = (dateStr: string) => { if (!dateStr) return "-"; return dateStr.split("T")[0]; }; const formatNumber = (val: string | number) => { const num = Number(val); return isNaN(num) ? "0" : num.toLocaleString(); }; return (
{/* 검색 영역 */}
setSearchDateFrom(e.target.value)} /> ~ setSearchDateTo(e.target.value)} />
setSearchCustomer(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} />
setSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} />
{/* 테이블 + 상세 패널 */}
출하계획 목록 {data.length}건
{loading ? (
) : ( 0 && checkedIds.length === data.filter(p => p.status !== "CANCELLED").length} onCheckedChange={handleCheckAll} /> No 상태 수주번호 거래처 품목코드 품목명 수주수량 계획수량 출하계획일 납기일 {data.length === 0 ? ( 출하계획이 없습니다. ) : ( data.map((plan, idx) => ( handleRowClick(plan)} > e.stopPropagation()}> handleCheck(plan.id, c as boolean)} disabled={plan.status === "CANCELLED"} /> {idx + 1} {getStatusLabel(plan.status)} {plan.order_no || "-"} {plan.customer_name || "-"} {plan.part_code || "-"} {plan.part_name || "-"} {formatNumber(plan.order_qty)} {formatNumber(plan.plan_qty)} {formatDate(plan.plan_date)} {formatDate(plan.due_date)} )) )}
)}
{/* 상세 패널 */} {selectedId && selectedPlan && ( <>
{selectedPlan.shipment_plan_no || `#${selectedPlan.id}`}
{/* 기본 정보 */}

기본 정보

출하계획번호 {selectedPlan.shipment_plan_no || "-"}
상태 {getStatusLabel(selectedPlan.status)}
수주번호 {selectedPlan.order_no || "-"}
거래처 {selectedPlan.customer_name || "-"}
등록일 {formatDate(selectedPlan.created_date)}
납기일 {formatDate(selectedPlan.due_date)}
{/* 품목 정보 */}

품목 정보

품목코드 {selectedPlan.part_code || "-"}
품목명 {selectedPlan.part_name || "-"}
규격 {selectedPlan.spec || "-"}
재질 {selectedPlan.material || "-"}
{/* 수량 정보 */}

수량 정보

수주수량 {formatNumber(selectedPlan.order_qty)}
{ setEditPlanQty(e.target.value); setIsDetailChanged(true); }} disabled={selectedPlan.status === "CANCELLED" || selectedPlan.status === "COMPLETED"} />
출하수량 {formatNumber(selectedPlan.shipped_qty)}
잔여수량 0 ? "text-destructive" : "text-emerald-600" )}> {formatNumber(Number(selectedPlan.plan_qty) - Number(selectedPlan.shipped_qty))}
{/* 출하 정보 */}

출하 정보

{ setEditPlanDate(e.target.value); setIsDetailChanged(true); }} disabled={selectedPlan.status === "CANCELLED" || selectedPlan.status === "COMPLETED"} />