"use client"; /** * ShippingPlanModal — 출하계획 모달 컴포넌트 * * 기존 shipping-plan 페이지의 기능을 Dialog 안에 재구현. * orderNo prop이 있으면 해당 수주의 출하계획만 필터링하여 표시. */ 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 { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Search, X, Save, RotateCcw, Loader2, Maximize2, Minimize2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { getShipmentPlanList, updateShipmentPlan, type ShipmentPlanListItem, } from "@/lib/api/shipping"; interface ShippingPlanModalProps { open: boolean; onOpenChange: (open: boolean) => void; orderNo?: string; onUpdated?: () => void; } 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"; } }; 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(); }; export function ShippingPlanModal({ open, onOpenChange, orderNo, onUpdated }: ShippingPlanModalProps) { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [selectedId, setSelectedId] = useState(null); const [checkedIds, setCheckedIds] = useState([]); const [isFullscreen, setIsFullscreen] = useState(false); // 검색 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(() => { if (!open) return; 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]); setSearchStatus("all"); setSearchCustomer(""); setSearchKeyword(orderNo || ""); setSelectedId(null); setCheckedIds([]); setIsDetailChanged(false); setIsFullscreen(false); }, [open, orderNo]); // 데이터 조회 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 (open && searchDateFrom && searchDateTo) { fetchData(); } }, [open, searchDateFrom, searchDateTo]); // eslint-disable-line react-hooks/exhaustive-deps const handleSearch = () => fetchData(); const handleResetSearch = () => { setSearchStatus("all"); setSearchCustomer(""); setSearchKeyword(orderNo || ""); 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 groupedData = useMemo(() => { const orderMap = new Map(); const orderKeys: string[] = []; data.forEach((plan) => { const key = plan.order_no || `_no_order_${plan.id}`; if (!orderMap.has(key)) { orderMap.set(key, []); orderKeys.push(key); } orderMap.get(key)!.push(plan); }); return orderKeys.map((key) => ({ orderNo: key, plans: orderMap.get(key)!, })); }, [data]); 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(); onUpdated?.(); } else { alert(result.message || "저장 실패"); } } catch (err: any) { alert(err.message || "저장 중 오류 발생"); } finally { setSaving(false); } }; return ( e.preventDefault()} >
출하계획 관리 {orderNo ? `수주번호 ${orderNo}의 출하계획` : "전체 출하계획을 관리합니다."}
{/* 검색 영역 */}
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} /> 수주번호 납기일 거래처 품목코드 품목명 수주수량 계획수량 출하계획일 상태 {groupedData.length === 0 ? ( 출하계획이 없습니다. ) : ( groupedData.map((group) => group.plans.map((plan, planIdx) => ( handleRowClick(plan)} > e.stopPropagation()}> {planIdx === 0 && ( checkedIds.includes(p.id))} onCheckedChange={(c) => { if (c) { setCheckedIds((prev) => [...new Set([...prev, ...group.plans.filter((p) => p.status !== "CANCELLED").map((p) => p.id)])]); } else { setCheckedIds((prev) => prev.filter((id) => !group.plans.some((p) => p.id === id))); } }} /> )} {planIdx === 0 ? (plan.order_no || "-") : ""} {planIdx === 0 ? formatDate(plan.due_date) : ""} {planIdx === 0 ? (plan.customer_name || "-") : ""} {plan.part_code || "-"} {plan.part_name || "-"} {formatNumber(plan.order_qty)} {formatNumber(plan.plan_qty)} {formatDate(plan.plan_date)} {getStatusLabel(plan.status)} )) ) )}
)}
{/* 상세 패널 */} {selectedId && selectedPlan && ( <>
{selectedPlan.shipment_plan_no || `#${selectedPlan.id}`}
{/* 기본 정보 */}

기본 정보

상태 {getStatusLabel(selectedPlan.status)}
수주번호 {selectedPlan.order_no || "-"}
거래처 {selectedPlan.customer_name || "-"}
납기일 {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"} />