"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"; // Card 제거 — rounded-lg border bg-card 패턴 사용 import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { Search, RotateCcw, Package, ClipboardList, Factory, MapPin, AlertTriangle, CheckCircle2, Loader2, Inbox, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { getWorkOrders, getMaterialStatus, getWarehouses, type WorkOrder, type MaterialData, type WarehouseData, } from "@/lib/api/materialStatus"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { apiClient } from "@/lib/api/client"; const GRID_COLUMNS = [ { key: "plan_no", label: "계획번호" }, { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "plan_qty", label: "수량" }, { key: "plan_date", label: "일자" }, { key: "status", label: "상태" }, ]; const formatDate = (date: Date) => { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; }; const FALLBACK_STATUS_MAP: Record = { planned: "계획", in_progress: "진행중", completed: "완료", pending: "대기", cancelled: "취소", }; const STATUS_STYLE_MAP: Record = { planned: "bg-secondary text-secondary-foreground border-border", pending: "bg-secondary text-secondary-foreground border-border", in_progress: "bg-primary/10 text-primary border-primary/20", completed: "bg-accent text-accent-foreground border-accent/50", cancelled: "bg-muted text-muted-foreground border-border", }; // 카테고리 라벨 기반으로 스타일 매칭 const LABEL_STYLE_MAP: Record = { "일반": "bg-secondary text-secondary-foreground border-border", "긴급": "bg-destructive/10 text-destructive border-destructive/20", "계획": "bg-secondary text-secondary-foreground border-border", "대기": "bg-secondary text-secondary-foreground border-border", "진행중": "bg-primary/10 text-primary border-primary/20", "완료": "bg-accent text-accent-foreground border-accent/50", "취소": "bg-muted text-muted-foreground border-border", }; export default function MaterialStatusPage() { const ts = useTableSettings("c16-material-status", "work_instruction", GRID_COLUMNS); const today = new Date(); const monthAgo = new Date(today); monthAgo.setMonth(today.getMonth() - 1); const [searchDateFrom, setSearchDateFrom] = useState(formatDate(monthAgo)); const [searchDateTo, setSearchDateTo] = useState(formatDate(today)); const [searchItemCode, setSearchItemCode] = useState(""); const [searchItemName, setSearchItemName] = useState(""); // 카테고리 코드→라벨 매핑 const [statusMap, setStatusMap] = useState>({}); useEffect(() => { (async () => { try { const res = await apiClient.get("/table-categories/work_instruction/status/values"); if (res.data?.success && res.data.data?.length > 0) { const map: Record = {}; const flatten = (vals: any[]) => { for (const v of vals) { map[v.valueCode] = v.valueLabel; if (v.children?.length) flatten(v.children); } }; flatten(res.data.data); setStatusMap(map); } } catch { /* ignore */ } })(); }, []); const getStatusLabel = useCallback((status: string) => { return statusMap[status] || FALLBACK_STATUS_MAP[status] || status; }, [statusMap]); const [workOrders, setWorkOrders] = useState([]); const [workOrdersLoading, setWorkOrdersLoading] = useState(false); const [checkedWoIds, setCheckedWoIds] = useState([]); const [selectedWoId, setSelectedWoId] = useState(null); const [warehouses, setWarehouses] = useState([]); const [warehouse, setWarehouse] = useState(""); const [materialSearch, setMaterialSearch] = useState(""); const [showShortageOnly, setShowShortageOnly] = useState(false); const [materials, setMaterials] = useState([]); const [materialsLoading, setMaterialsLoading] = useState(false); // 창고 목록 초기 로드 useEffect(() => { (async () => { const res = await getWarehouses(); if (res.success && res.data) { setWarehouses(res.data); } })(); }, []); // 작업지시 검색 const handleSearch = useCallback(async () => { setWorkOrdersLoading(true); try { const res = await getWorkOrders({ dateFrom: searchDateFrom, dateTo: searchDateTo, itemCode: searchItemCode || undefined, itemName: searchItemName || undefined, }); if (res.success && res.data) { setWorkOrders(res.data); setCheckedWoIds([]); setSelectedWoId(null); setMaterials([]); } } finally { setWorkOrdersLoading(false); } }, [searchDateFrom, searchDateTo, searchItemCode, searchItemName]); // 초기 로드 useEffect(() => { handleSearch(); }, []); const isAllChecked = workOrders.length > 0 && checkedWoIds.length === workOrders.length; const handleCheckAll = useCallback( (checked: boolean) => { setCheckedWoIds(checked ? workOrders.map((wo) => wo.id) : []); }, [workOrders] ); const handleCheckWo = useCallback((id: string, checked: boolean) => { setCheckedWoIds((prev) => checked ? [...prev, id] : prev.filter((i) => i !== id) ); }, []); const handleSelectWo = useCallback((id: string) => { setSelectedWoId((prev) => (prev === id ? null : id)); }, []); // 선택된 작업지시의 자재 조회 const handleLoadSelectedMaterials = useCallback(async () => { if (checkedWoIds.length === 0) { alert("자재를 조회할 작업지시를 선택해주세요."); return; } setMaterialsLoading(true); try { const res = await getMaterialStatus({ planIds: checkedWoIds, warehouseCode: warehouse || undefined, }); if (res.success && res.data) { setMaterials(res.data); } } finally { setMaterialsLoading(false); } }, [checkedWoIds, warehouse]); const handleResetSearch = useCallback(() => { const t = new Date(); const m = new Date(t); m.setMonth(t.getMonth() - 1); setSearchDateFrom(formatDate(m)); setSearchDateTo(formatDate(t)); setSearchItemCode(""); setSearchItemName(""); setMaterialSearch(""); setShowShortageOnly(false); }, []); const filteredMaterials = useMemo(() => { return materials.filter((m) => { const searchLower = materialSearch.toLowerCase(); const matchesSearch = !materialSearch || m.code.toLowerCase().includes(searchLower) || m.name.toLowerCase().includes(searchLower); const matchesShortage = !showShortageOnly || m.current < m.required; return matchesSearch && matchesShortage; }); }, [materials, materialSearch, showShortageOnly]); return (
{/* 검색 영역 */}
기간
setSearchDateFrom(e.target.value)} /> ~ setSearchDateTo(e.target.value)} />
품목코드 setSearchItemCode(e.target.value)} />
품목명 setSearchItemName(e.target.value)} />
{/* 메인 콘텐츠 (좌우 분할) */}
{/* 왼쪽: 작업지시 리스트 */}
{/* 패널 헤더 */}
작업지시 리스트
{workOrders.length}건
{/* 작업지시 목록 */}
{workOrdersLoading ? (

작업지시를 조회하고 있어요...

) : workOrders.length === 0 ? (

조회된 작업지시가 없어요

) : ( ts.groupData(workOrders).map((wo) => { if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; return (
handleSelectWo(wo.id)} >
e.stopPropagation()} > handleCheckWo(wo.id, c as boolean) } />
{ts.isVisible("plan_no") && ( {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} )} {ts.isVisible("status") && ( {getStatusLabel(wo.status)} )}
{ts.isVisible("item_name") && ( {wo.item_name} )} {ts.isVisible("item_code") && ( ({wo.item_code}) )}
{ts.isVisible("plan_qty") && ( <> 수량: {Number(wo.plan_qty).toLocaleString()}개 )} {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( | )} {ts.isVisible("plan_date") && ( <> 일자: {wo.plan_date ? new Date(wo.plan_date) .toISOString() .slice(0, 10) : "-"} )}
); }) )}
{/* 오른쪽: 원자재 현황 */}
{/* 패널 헤더 */}
원자재 재고 현황
{/* 필터 */}
setMaterialSearch(e.target.value)} /> {filteredMaterials.length}개 품목
{/* 원자재 목록 */}
{materialsLoading ? (

자재현황을 조회하고 있어요...

) : materials.length === 0 ? (

작업지시를 선택하고 자재조회 버튼을 클릭해주세요

) : filteredMaterials.length === 0 ? (

조회된 원자재가 없어요

) : ( filteredMaterials.map((material) => { const shortage = material.required - material.current; const isShortage = shortage > 0; const percentage = material.required > 0 ? Math.min( (material.current / material.required) * 100, 100 ) : 100; return (
{/* 메인 정보 라인 */}
{material.name} ({material.code}) | 필요: {material.required.toLocaleString()} {material.unit} | 현재: {material.current.toLocaleString()} {material.unit} | {isShortage ? "부족:" : "여유:"} {Math.abs(shortage).toLocaleString()} {material.unit} ({percentage.toFixed(0)}%) {isShortage ? ( 부족 ) : ( 충분 )}
{/* 위치별 재고 */} {material.locations.length > 0 && (
{material.locations.map((loc, idx) => ( {loc.location || loc.warehouse} {loc.qty.toLocaleString()} {material.unit} ))}
)}
); }) )}
); }