diff --git a/frontend/app/(main)/COMPANY_10/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/inbound-outbound/page.tsx new file mode 100644 index 00000000..fed63cef --- /dev/null +++ b/frontend/app/(main)/COMPANY_10/logistics/inbound-outbound/page.tsx @@ -0,0 +1,379 @@ +"use client"; + +/** + * 입출고관리 — 하드코딩 페이지 + * + * inventory_history 테이블 기반 입고+출고 통합 조회 + * 그룹핑, 검색, 엑셀 다운로드 지원 + */ + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Download, Loader2, Inbox, ChevronDown, ChevronRight, + ArrowDownToLine, ArrowUpFromLine, Package, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; + +const HISTORY_TABLE = "inventory_history"; + +const fmtNum = (v: any) => { + const n = Number(v); + return isNaN(n) ? "0" : n.toLocaleString(); +}; + +const fmtDate = (v: any) => { + if (!v) return "-"; + const s = String(v); + const m = s.match(/(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); +}; + +export default function InboundOutboundPage() { + const { user } = useAuth(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [groupBy, setGroupBy] = useState("none"); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [checkedIds, setCheckedIds] = useState>(new Set()); + + // 품목명/단위 캐시 + const [itemMap, setItemMap] = useState>({}); + const [warehouseMap, setWarehouseMap] = useState>({}); + const [userMap, setUserMap] = useState>({}); + + // ════════ 데이터 로드 ════════ + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = []; + if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); + if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); + + for (const f of searchFilters) { + if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); + } + + const res = await apiClient.post(`/table-management/tables/${HISTORY_TABLE}/data`, { + page: 1, size: 1000, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + sort: { columnName: "transaction_date", order: "desc" }, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + + // 품목 정보 조회 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; + if (itemCodes.length > 0) { + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: itemCodes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + } + setItemMap(map); + } catch { /* skip */ } + } + + // 창고 정보 조회 + try { + const whRes = await apiClient.post(`/table-management/tables/warehouse_info/data`, { + page: 1, size: 100, autoFilter: true, + }); + const whs = whRes.data?.data?.data || whRes.data?.data?.rows || []; + const whMap: Record = {}; + for (const w of whs) whMap[w.warehouse_code] = w.warehouse_name || w.warehouse_code; + setWarehouseMap(whMap); + } catch { /* skip */ } + + // 사용자 정보 조회 (writer → user_name 변환) + const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))]; + if (writerIds.length > 0) { + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, size: 500, autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + const uMap: Record = {}; + for (const u of users) uMap[u.user_id] = u.user_name || u.user_id; + setUserMap(uMap); + } catch { /* skip */ } + } + } catch { + toast.error("입출고 내역 조회 실패"); + } finally { + setLoading(false); + } + }, [searchFilters, typeFilter, categoryFilter]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // ════════ 카테고리 목록 (remark에서 추출) ════════ + + const categoryOptions = useMemo(() => { + const set = new Set(); + data.forEach((r) => { if (r.remark) set.add(r.remark); }); + return Array.from(set).sort(); + }, [data]); + + // ════════ 그룹핑 ════════ + + const groupedData = useMemo(() => { + if (groupBy === "none") return data; + const groups = new Map(); + for (const row of data) { + let key: string; + switch (groupBy) { + case "transaction_type": key = row.transaction_type || "미지정"; break; + case "remark": key = row.remark || "미지정"; break; + case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: key = row[groupBy] || "미지정"; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + const result: any[] = []; + groups.forEach((items, gk) => { + const totalQty = items.reduce((s, r) => s + (Number(r.quantity) || 0), 0); + result.push({ _group: true, _key: gk, _count: items.length, _totalQty: totalQty }); + result.push(...items); + }); + if (expandedGroups.size === 0) setExpandedGroups(new Set(Array.from(groups.keys()))); + return result; + }, [data, groupBy, itemMap, warehouseMap]); + + const toggleGroup = (key: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // ════════ 체크박스 ════════ + + const allIds = data.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.has(id)); + const toggleAll = (checked: boolean) => setCheckedIds(checked ? new Set(allIds) : new Set()); + const toggleOne = (id: string) => { + setCheckedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + // ════════ 엑셀 ════════ + + const handleExcel = async () => { + if (data.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return; } + const rows = data.map((r, i) => ({ + No: i + 1, + 입출고구분: r.transaction_type || "", + 카테고리: r.remark || "", + 처리일자: fmtDate(r.transaction_date), + 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", + 위치: r.location_code || "", + 품목코드: r.item_code || "", + 품목명: itemMap[r.item_code]?.item_name || "", + 수량: Number(r.quantity) || 0, + 단위: itemMap[r.item_code]?.unit || "", + 로트번호: r.lot_number || "", + 참조번호: r.reference_number || "", + 담당자: r.manager_name || userMap[r.writer] || r.writer || "", + })); + const _n = new Date(); + await exportToExcel(rows, `입출고관리_${_n.getFullYear()}${String(_n.getMonth() + 1).padStart(2, "0")}${String(_n.getDate()).padStart(2, "0")}.xlsx`, "입출고내역"); + toast.success("다운로드 완료"); + }; + + // ════════ 렌더 ════════ + + return ( +
+ {/* 검색 */} +
+ + + + +
+ +
+
+ + {/* 데이터 테이블 */} +
+ {/* 헤더 */} +
+
+ + 입출고 내역 + {data.length}건 +
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+ ) : data.length === 0 ? ( +
+ + 조회된 데이터가 없습니다 +
+ ) : ( + + + + + toggleAll(!!c)} /> + + 입출고구분 + 카테고리 + 처리일자 + 창고 + 위치 + 품목코드 + 품목명 + 수량 + 단위 + 로트번호 + 참조번호 + 담당자 + + + + {groupedData.map((row, idx) => { + if (row._group) { + const expanded = expandedGroups.has(row._key); + return ( + toggleGroup(row._key)}> + +
+ {expanded ? : } + {row._key} + {row._count}건 +
+
+ + {fmtNum(row._totalQty)} + + +
+ ); + } + // 그룹 접힘 체크 + if (groupBy !== "none") { + let gk: string; + switch (groupBy) { + case "warehouse_code": gk = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": gk = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: gk = row[groupBy] || "미지정"; + } + if (!expandedGroups.has(gk)) return null; + } + + const isIn = row.transaction_type === "입고"; + const qty = Number(row.quantity) || 0; + const checked = checkedIds.has(row.id); + const info = itemMap[row.item_code]; + + return ( + + + toggleOne(row.id)} /> + + + + {isIn ? : } + {row.transaction_type} + + + {row.remark || "-"} + {fmtDate(row.transaction_date)} + {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} + {row.location_code || "-"} + {row.item_code || "-"} + {info?.item_name || "-"} + + {isIn ? "+" : ""}{fmtNum(qty)} + + {info?.unit || "-"} + {row.lot_number || "-"} + {row.reference_number || "-"} + {row.manager_name || userMap[row.writer] || row.writer || "-"} + + ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_16/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/inbound-outbound/page.tsx new file mode 100644 index 00000000..fed63cef --- /dev/null +++ b/frontend/app/(main)/COMPANY_16/logistics/inbound-outbound/page.tsx @@ -0,0 +1,379 @@ +"use client"; + +/** + * 입출고관리 — 하드코딩 페이지 + * + * inventory_history 테이블 기반 입고+출고 통합 조회 + * 그룹핑, 검색, 엑셀 다운로드 지원 + */ + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Download, Loader2, Inbox, ChevronDown, ChevronRight, + ArrowDownToLine, ArrowUpFromLine, Package, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; + +const HISTORY_TABLE = "inventory_history"; + +const fmtNum = (v: any) => { + const n = Number(v); + return isNaN(n) ? "0" : n.toLocaleString(); +}; + +const fmtDate = (v: any) => { + if (!v) return "-"; + const s = String(v); + const m = s.match(/(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); +}; + +export default function InboundOutboundPage() { + const { user } = useAuth(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [groupBy, setGroupBy] = useState("none"); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [checkedIds, setCheckedIds] = useState>(new Set()); + + // 품목명/단위 캐시 + const [itemMap, setItemMap] = useState>({}); + const [warehouseMap, setWarehouseMap] = useState>({}); + const [userMap, setUserMap] = useState>({}); + + // ════════ 데이터 로드 ════════ + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = []; + if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); + if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); + + for (const f of searchFilters) { + if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); + } + + const res = await apiClient.post(`/table-management/tables/${HISTORY_TABLE}/data`, { + page: 1, size: 1000, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + sort: { columnName: "transaction_date", order: "desc" }, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + + // 품목 정보 조회 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; + if (itemCodes.length > 0) { + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: itemCodes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + } + setItemMap(map); + } catch { /* skip */ } + } + + // 창고 정보 조회 + try { + const whRes = await apiClient.post(`/table-management/tables/warehouse_info/data`, { + page: 1, size: 100, autoFilter: true, + }); + const whs = whRes.data?.data?.data || whRes.data?.data?.rows || []; + const whMap: Record = {}; + for (const w of whs) whMap[w.warehouse_code] = w.warehouse_name || w.warehouse_code; + setWarehouseMap(whMap); + } catch { /* skip */ } + + // 사용자 정보 조회 (writer → user_name 변환) + const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))]; + if (writerIds.length > 0) { + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, size: 500, autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + const uMap: Record = {}; + for (const u of users) uMap[u.user_id] = u.user_name || u.user_id; + setUserMap(uMap); + } catch { /* skip */ } + } + } catch { + toast.error("입출고 내역 조회 실패"); + } finally { + setLoading(false); + } + }, [searchFilters, typeFilter, categoryFilter]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // ════════ 카테고리 목록 (remark에서 추출) ════════ + + const categoryOptions = useMemo(() => { + const set = new Set(); + data.forEach((r) => { if (r.remark) set.add(r.remark); }); + return Array.from(set).sort(); + }, [data]); + + // ════════ 그룹핑 ════════ + + const groupedData = useMemo(() => { + if (groupBy === "none") return data; + const groups = new Map(); + for (const row of data) { + let key: string; + switch (groupBy) { + case "transaction_type": key = row.transaction_type || "미지정"; break; + case "remark": key = row.remark || "미지정"; break; + case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: key = row[groupBy] || "미지정"; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + const result: any[] = []; + groups.forEach((items, gk) => { + const totalQty = items.reduce((s, r) => s + (Number(r.quantity) || 0), 0); + result.push({ _group: true, _key: gk, _count: items.length, _totalQty: totalQty }); + result.push(...items); + }); + if (expandedGroups.size === 0) setExpandedGroups(new Set(Array.from(groups.keys()))); + return result; + }, [data, groupBy, itemMap, warehouseMap]); + + const toggleGroup = (key: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // ════════ 체크박스 ════════ + + const allIds = data.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.has(id)); + const toggleAll = (checked: boolean) => setCheckedIds(checked ? new Set(allIds) : new Set()); + const toggleOne = (id: string) => { + setCheckedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + // ════════ 엑셀 ════════ + + const handleExcel = async () => { + if (data.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return; } + const rows = data.map((r, i) => ({ + No: i + 1, + 입출고구분: r.transaction_type || "", + 카테고리: r.remark || "", + 처리일자: fmtDate(r.transaction_date), + 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", + 위치: r.location_code || "", + 품목코드: r.item_code || "", + 품목명: itemMap[r.item_code]?.item_name || "", + 수량: Number(r.quantity) || 0, + 단위: itemMap[r.item_code]?.unit || "", + 로트번호: r.lot_number || "", + 참조번호: r.reference_number || "", + 담당자: r.manager_name || userMap[r.writer] || r.writer || "", + })); + const _n = new Date(); + await exportToExcel(rows, `입출고관리_${_n.getFullYear()}${String(_n.getMonth() + 1).padStart(2, "0")}${String(_n.getDate()).padStart(2, "0")}.xlsx`, "입출고내역"); + toast.success("다운로드 완료"); + }; + + // ════════ 렌더 ════════ + + return ( +
+ {/* 검색 */} +
+ + + + +
+ +
+
+ + {/* 데이터 테이블 */} +
+ {/* 헤더 */} +
+
+ + 입출고 내역 + {data.length}건 +
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+ ) : data.length === 0 ? ( +
+ + 조회된 데이터가 없습니다 +
+ ) : ( + + + + + toggleAll(!!c)} /> + + 입출고구분 + 카테고리 + 처리일자 + 창고 + 위치 + 품목코드 + 품목명 + 수량 + 단위 + 로트번호 + 참조번호 + 담당자 + + + + {groupedData.map((row, idx) => { + if (row._group) { + const expanded = expandedGroups.has(row._key); + return ( + toggleGroup(row._key)}> + +
+ {expanded ? : } + {row._key} + {row._count}건 +
+
+ + {fmtNum(row._totalQty)} + + +
+ ); + } + // 그룹 접힘 체크 + if (groupBy !== "none") { + let gk: string; + switch (groupBy) { + case "warehouse_code": gk = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": gk = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: gk = row[groupBy] || "미지정"; + } + if (!expandedGroups.has(gk)) return null; + } + + const isIn = row.transaction_type === "입고"; + const qty = Number(row.quantity) || 0; + const checked = checkedIds.has(row.id); + const info = itemMap[row.item_code]; + + return ( + + + toggleOne(row.id)} /> + + + + {isIn ? : } + {row.transaction_type} + + + {row.remark || "-"} + {fmtDate(row.transaction_date)} + {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} + {row.location_code || "-"} + {row.item_code || "-"} + {info?.item_name || "-"} + + {isIn ? "+" : ""}{fmtNum(qty)} + + {info?.unit || "-"} + {row.lot_number || "-"} + {row.reference_number || "-"} + {row.manager_name || userMap[row.writer] || row.writer || "-"} + + ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_29/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/inbound-outbound/page.tsx new file mode 100644 index 00000000..fed63cef --- /dev/null +++ b/frontend/app/(main)/COMPANY_29/logistics/inbound-outbound/page.tsx @@ -0,0 +1,379 @@ +"use client"; + +/** + * 입출고관리 — 하드코딩 페이지 + * + * inventory_history 테이블 기반 입고+출고 통합 조회 + * 그룹핑, 검색, 엑셀 다운로드 지원 + */ + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Download, Loader2, Inbox, ChevronDown, ChevronRight, + ArrowDownToLine, ArrowUpFromLine, Package, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; + +const HISTORY_TABLE = "inventory_history"; + +const fmtNum = (v: any) => { + const n = Number(v); + return isNaN(n) ? "0" : n.toLocaleString(); +}; + +const fmtDate = (v: any) => { + if (!v) return "-"; + const s = String(v); + const m = s.match(/(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); +}; + +export default function InboundOutboundPage() { + const { user } = useAuth(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [groupBy, setGroupBy] = useState("none"); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [checkedIds, setCheckedIds] = useState>(new Set()); + + // 품목명/단위 캐시 + const [itemMap, setItemMap] = useState>({}); + const [warehouseMap, setWarehouseMap] = useState>({}); + const [userMap, setUserMap] = useState>({}); + + // ════════ 데이터 로드 ════════ + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = []; + if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); + if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); + + for (const f of searchFilters) { + if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); + } + + const res = await apiClient.post(`/table-management/tables/${HISTORY_TABLE}/data`, { + page: 1, size: 1000, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + sort: { columnName: "transaction_date", order: "desc" }, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + + // 품목 정보 조회 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; + if (itemCodes.length > 0) { + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: itemCodes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + } + setItemMap(map); + } catch { /* skip */ } + } + + // 창고 정보 조회 + try { + const whRes = await apiClient.post(`/table-management/tables/warehouse_info/data`, { + page: 1, size: 100, autoFilter: true, + }); + const whs = whRes.data?.data?.data || whRes.data?.data?.rows || []; + const whMap: Record = {}; + for (const w of whs) whMap[w.warehouse_code] = w.warehouse_name || w.warehouse_code; + setWarehouseMap(whMap); + } catch { /* skip */ } + + // 사용자 정보 조회 (writer → user_name 변환) + const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))]; + if (writerIds.length > 0) { + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, size: 500, autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + const uMap: Record = {}; + for (const u of users) uMap[u.user_id] = u.user_name || u.user_id; + setUserMap(uMap); + } catch { /* skip */ } + } + } catch { + toast.error("입출고 내역 조회 실패"); + } finally { + setLoading(false); + } + }, [searchFilters, typeFilter, categoryFilter]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // ════════ 카테고리 목록 (remark에서 추출) ════════ + + const categoryOptions = useMemo(() => { + const set = new Set(); + data.forEach((r) => { if (r.remark) set.add(r.remark); }); + return Array.from(set).sort(); + }, [data]); + + // ════════ 그룹핑 ════════ + + const groupedData = useMemo(() => { + if (groupBy === "none") return data; + const groups = new Map(); + for (const row of data) { + let key: string; + switch (groupBy) { + case "transaction_type": key = row.transaction_type || "미지정"; break; + case "remark": key = row.remark || "미지정"; break; + case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: key = row[groupBy] || "미지정"; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + const result: any[] = []; + groups.forEach((items, gk) => { + const totalQty = items.reduce((s, r) => s + (Number(r.quantity) || 0), 0); + result.push({ _group: true, _key: gk, _count: items.length, _totalQty: totalQty }); + result.push(...items); + }); + if (expandedGroups.size === 0) setExpandedGroups(new Set(Array.from(groups.keys()))); + return result; + }, [data, groupBy, itemMap, warehouseMap]); + + const toggleGroup = (key: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // ════════ 체크박스 ════════ + + const allIds = data.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.has(id)); + const toggleAll = (checked: boolean) => setCheckedIds(checked ? new Set(allIds) : new Set()); + const toggleOne = (id: string) => { + setCheckedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + // ════════ 엑셀 ════════ + + const handleExcel = async () => { + if (data.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return; } + const rows = data.map((r, i) => ({ + No: i + 1, + 입출고구분: r.transaction_type || "", + 카테고리: r.remark || "", + 처리일자: fmtDate(r.transaction_date), + 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", + 위치: r.location_code || "", + 품목코드: r.item_code || "", + 품목명: itemMap[r.item_code]?.item_name || "", + 수량: Number(r.quantity) || 0, + 단위: itemMap[r.item_code]?.unit || "", + 로트번호: r.lot_number || "", + 참조번호: r.reference_number || "", + 담당자: r.manager_name || userMap[r.writer] || r.writer || "", + })); + const _n = new Date(); + await exportToExcel(rows, `입출고관리_${_n.getFullYear()}${String(_n.getMonth() + 1).padStart(2, "0")}${String(_n.getDate()).padStart(2, "0")}.xlsx`, "입출고내역"); + toast.success("다운로드 완료"); + }; + + // ════════ 렌더 ════════ + + return ( +
+ {/* 검색 */} +
+ + + + +
+ +
+
+ + {/* 데이터 테이블 */} +
+ {/* 헤더 */} +
+
+ + 입출고 내역 + {data.length}건 +
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+ ) : data.length === 0 ? ( +
+ + 조회된 데이터가 없습니다 +
+ ) : ( + + + + + toggleAll(!!c)} /> + + 입출고구분 + 카테고리 + 처리일자 + 창고 + 위치 + 품목코드 + 품목명 + 수량 + 단위 + 로트번호 + 참조번호 + 담당자 + + + + {groupedData.map((row, idx) => { + if (row._group) { + const expanded = expandedGroups.has(row._key); + return ( + toggleGroup(row._key)}> + +
+ {expanded ? : } + {row._key} + {row._count}건 +
+
+ + {fmtNum(row._totalQty)} + + +
+ ); + } + // 그룹 접힘 체크 + if (groupBy !== "none") { + let gk: string; + switch (groupBy) { + case "warehouse_code": gk = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": gk = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: gk = row[groupBy] || "미지정"; + } + if (!expandedGroups.has(gk)) return null; + } + + const isIn = row.transaction_type === "입고"; + const qty = Number(row.quantity) || 0; + const checked = checkedIds.has(row.id); + const info = itemMap[row.item_code]; + + return ( + + + toggleOne(row.id)} /> + + + + {isIn ? : } + {row.transaction_type} + + + {row.remark || "-"} + {fmtDate(row.transaction_date)} + {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} + {row.location_code || "-"} + {row.item_code || "-"} + {info?.item_name || "-"} + + {isIn ? "+" : ""}{fmtNum(qty)} + + {info?.unit || "-"} + {row.lot_number || "-"} + {row.reference_number || "-"} + {row.manager_name || userMap[row.writer] || row.writer || "-"} + + ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_30/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/inbound-outbound/page.tsx new file mode 100644 index 00000000..fed63cef --- /dev/null +++ b/frontend/app/(main)/COMPANY_30/logistics/inbound-outbound/page.tsx @@ -0,0 +1,379 @@ +"use client"; + +/** + * 입출고관리 — 하드코딩 페이지 + * + * inventory_history 테이블 기반 입고+출고 통합 조회 + * 그룹핑, 검색, 엑셀 다운로드 지원 + */ + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Download, Loader2, Inbox, ChevronDown, ChevronRight, + ArrowDownToLine, ArrowUpFromLine, Package, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; + +const HISTORY_TABLE = "inventory_history"; + +const fmtNum = (v: any) => { + const n = Number(v); + return isNaN(n) ? "0" : n.toLocaleString(); +}; + +const fmtDate = (v: any) => { + if (!v) return "-"; + const s = String(v); + const m = s.match(/(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); +}; + +export default function InboundOutboundPage() { + const { user } = useAuth(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [groupBy, setGroupBy] = useState("none"); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [checkedIds, setCheckedIds] = useState>(new Set()); + + // 품목명/단위 캐시 + const [itemMap, setItemMap] = useState>({}); + const [warehouseMap, setWarehouseMap] = useState>({}); + const [userMap, setUserMap] = useState>({}); + + // ════════ 데이터 로드 ════════ + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = []; + if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); + if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); + + for (const f of searchFilters) { + if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); + } + + const res = await apiClient.post(`/table-management/tables/${HISTORY_TABLE}/data`, { + page: 1, size: 1000, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + sort: { columnName: "transaction_date", order: "desc" }, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + + // 품목 정보 조회 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; + if (itemCodes.length > 0) { + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: itemCodes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + } + setItemMap(map); + } catch { /* skip */ } + } + + // 창고 정보 조회 + try { + const whRes = await apiClient.post(`/table-management/tables/warehouse_info/data`, { + page: 1, size: 100, autoFilter: true, + }); + const whs = whRes.data?.data?.data || whRes.data?.data?.rows || []; + const whMap: Record = {}; + for (const w of whs) whMap[w.warehouse_code] = w.warehouse_name || w.warehouse_code; + setWarehouseMap(whMap); + } catch { /* skip */ } + + // 사용자 정보 조회 (writer → user_name 변환) + const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))]; + if (writerIds.length > 0) { + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, size: 500, autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + const uMap: Record = {}; + for (const u of users) uMap[u.user_id] = u.user_name || u.user_id; + setUserMap(uMap); + } catch { /* skip */ } + } + } catch { + toast.error("입출고 내역 조회 실패"); + } finally { + setLoading(false); + } + }, [searchFilters, typeFilter, categoryFilter]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // ════════ 카테고리 목록 (remark에서 추출) ════════ + + const categoryOptions = useMemo(() => { + const set = new Set(); + data.forEach((r) => { if (r.remark) set.add(r.remark); }); + return Array.from(set).sort(); + }, [data]); + + // ════════ 그룹핑 ════════ + + const groupedData = useMemo(() => { + if (groupBy === "none") return data; + const groups = new Map(); + for (const row of data) { + let key: string; + switch (groupBy) { + case "transaction_type": key = row.transaction_type || "미지정"; break; + case "remark": key = row.remark || "미지정"; break; + case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: key = row[groupBy] || "미지정"; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + const result: any[] = []; + groups.forEach((items, gk) => { + const totalQty = items.reduce((s, r) => s + (Number(r.quantity) || 0), 0); + result.push({ _group: true, _key: gk, _count: items.length, _totalQty: totalQty }); + result.push(...items); + }); + if (expandedGroups.size === 0) setExpandedGroups(new Set(Array.from(groups.keys()))); + return result; + }, [data, groupBy, itemMap, warehouseMap]); + + const toggleGroup = (key: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // ════════ 체크박스 ════════ + + const allIds = data.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.has(id)); + const toggleAll = (checked: boolean) => setCheckedIds(checked ? new Set(allIds) : new Set()); + const toggleOne = (id: string) => { + setCheckedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + // ════════ 엑셀 ════════ + + const handleExcel = async () => { + if (data.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return; } + const rows = data.map((r, i) => ({ + No: i + 1, + 입출고구분: r.transaction_type || "", + 카테고리: r.remark || "", + 처리일자: fmtDate(r.transaction_date), + 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", + 위치: r.location_code || "", + 품목코드: r.item_code || "", + 품목명: itemMap[r.item_code]?.item_name || "", + 수량: Number(r.quantity) || 0, + 단위: itemMap[r.item_code]?.unit || "", + 로트번호: r.lot_number || "", + 참조번호: r.reference_number || "", + 담당자: r.manager_name || userMap[r.writer] || r.writer || "", + })); + const _n = new Date(); + await exportToExcel(rows, `입출고관리_${_n.getFullYear()}${String(_n.getMonth() + 1).padStart(2, "0")}${String(_n.getDate()).padStart(2, "0")}.xlsx`, "입출고내역"); + toast.success("다운로드 완료"); + }; + + // ════════ 렌더 ════════ + + return ( +
+ {/* 검색 */} +
+ + + + +
+ +
+
+ + {/* 데이터 테이블 */} +
+ {/* 헤더 */} +
+
+ + 입출고 내역 + {data.length}건 +
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+ ) : data.length === 0 ? ( +
+ + 조회된 데이터가 없습니다 +
+ ) : ( + + + + + toggleAll(!!c)} /> + + 입출고구분 + 카테고리 + 처리일자 + 창고 + 위치 + 품목코드 + 품목명 + 수량 + 단위 + 로트번호 + 참조번호 + 담당자 + + + + {groupedData.map((row, idx) => { + if (row._group) { + const expanded = expandedGroups.has(row._key); + return ( + toggleGroup(row._key)}> + +
+ {expanded ? : } + {row._key} + {row._count}건 +
+
+ + {fmtNum(row._totalQty)} + + +
+ ); + } + // 그룹 접힘 체크 + if (groupBy !== "none") { + let gk: string; + switch (groupBy) { + case "warehouse_code": gk = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": gk = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: gk = row[groupBy] || "미지정"; + } + if (!expandedGroups.has(gk)) return null; + } + + const isIn = row.transaction_type === "입고"; + const qty = Number(row.quantity) || 0; + const checked = checkedIds.has(row.id); + const info = itemMap[row.item_code]; + + return ( + + + toggleOne(row.id)} /> + + + + {isIn ? : } + {row.transaction_type} + + + {row.remark || "-"} + {fmtDate(row.transaction_date)} + {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} + {row.location_code || "-"} + {row.item_code || "-"} + {info?.item_name || "-"} + + {isIn ? "+" : ""}{fmtNum(qty)} + + {info?.unit || "-"} + {row.lot_number || "-"} + {row.reference_number || "-"} + {row.manager_name || userMap[row.writer] || row.writer || "-"} + + ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_7/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/inbound-outbound/page.tsx new file mode 100644 index 00000000..fed63cef --- /dev/null +++ b/frontend/app/(main)/COMPANY_7/logistics/inbound-outbound/page.tsx @@ -0,0 +1,379 @@ +"use client"; + +/** + * 입출고관리 — 하드코딩 페이지 + * + * inventory_history 테이블 기반 입고+출고 통합 조회 + * 그룹핑, 검색, 엑셀 다운로드 지원 + */ + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Download, Loader2, Inbox, ChevronDown, ChevronRight, + ArrowDownToLine, ArrowUpFromLine, Package, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; + +const HISTORY_TABLE = "inventory_history"; + +const fmtNum = (v: any) => { + const n = Number(v); + return isNaN(n) ? "0" : n.toLocaleString(); +}; + +const fmtDate = (v: any) => { + if (!v) return "-"; + const s = String(v); + const m = s.match(/(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); +}; + +export default function InboundOutboundPage() { + const { user } = useAuth(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [groupBy, setGroupBy] = useState("none"); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [checkedIds, setCheckedIds] = useState>(new Set()); + + // 품목명/단위 캐시 + const [itemMap, setItemMap] = useState>({}); + const [warehouseMap, setWarehouseMap] = useState>({}); + const [userMap, setUserMap] = useState>({}); + + // ════════ 데이터 로드 ════════ + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = []; + if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); + if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); + + for (const f of searchFilters) { + if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); + } + + const res = await apiClient.post(`/table-management/tables/${HISTORY_TABLE}/data`, { + page: 1, size: 1000, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + sort: { columnName: "transaction_date", order: "desc" }, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + + // 품목 정보 조회 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; + if (itemCodes.length > 0) { + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: itemCodes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + } + setItemMap(map); + } catch { /* skip */ } + } + + // 창고 정보 조회 + try { + const whRes = await apiClient.post(`/table-management/tables/warehouse_info/data`, { + page: 1, size: 100, autoFilter: true, + }); + const whs = whRes.data?.data?.data || whRes.data?.data?.rows || []; + const whMap: Record = {}; + for (const w of whs) whMap[w.warehouse_code] = w.warehouse_name || w.warehouse_code; + setWarehouseMap(whMap); + } catch { /* skip */ } + + // 사용자 정보 조회 (writer → user_name 변환) + const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))]; + if (writerIds.length > 0) { + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, size: 500, autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + const uMap: Record = {}; + for (const u of users) uMap[u.user_id] = u.user_name || u.user_id; + setUserMap(uMap); + } catch { /* skip */ } + } + } catch { + toast.error("입출고 내역 조회 실패"); + } finally { + setLoading(false); + } + }, [searchFilters, typeFilter, categoryFilter]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // ════════ 카테고리 목록 (remark에서 추출) ════════ + + const categoryOptions = useMemo(() => { + const set = new Set(); + data.forEach((r) => { if (r.remark) set.add(r.remark); }); + return Array.from(set).sort(); + }, [data]); + + // ════════ 그룹핑 ════════ + + const groupedData = useMemo(() => { + if (groupBy === "none") return data; + const groups = new Map(); + for (const row of data) { + let key: string; + switch (groupBy) { + case "transaction_type": key = row.transaction_type || "미지정"; break; + case "remark": key = row.remark || "미지정"; break; + case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: key = row[groupBy] || "미지정"; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + const result: any[] = []; + groups.forEach((items, gk) => { + const totalQty = items.reduce((s, r) => s + (Number(r.quantity) || 0), 0); + result.push({ _group: true, _key: gk, _count: items.length, _totalQty: totalQty }); + result.push(...items); + }); + if (expandedGroups.size === 0) setExpandedGroups(new Set(Array.from(groups.keys()))); + return result; + }, [data, groupBy, itemMap, warehouseMap]); + + const toggleGroup = (key: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // ════════ 체크박스 ════════ + + const allIds = data.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.has(id)); + const toggleAll = (checked: boolean) => setCheckedIds(checked ? new Set(allIds) : new Set()); + const toggleOne = (id: string) => { + setCheckedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + // ════════ 엑셀 ════════ + + const handleExcel = async () => { + if (data.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return; } + const rows = data.map((r, i) => ({ + No: i + 1, + 입출고구분: r.transaction_type || "", + 카테고리: r.remark || "", + 처리일자: fmtDate(r.transaction_date), + 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", + 위치: r.location_code || "", + 품목코드: r.item_code || "", + 품목명: itemMap[r.item_code]?.item_name || "", + 수량: Number(r.quantity) || 0, + 단위: itemMap[r.item_code]?.unit || "", + 로트번호: r.lot_number || "", + 참조번호: r.reference_number || "", + 담당자: r.manager_name || userMap[r.writer] || r.writer || "", + })); + const _n = new Date(); + await exportToExcel(rows, `입출고관리_${_n.getFullYear()}${String(_n.getMonth() + 1).padStart(2, "0")}${String(_n.getDate()).padStart(2, "0")}.xlsx`, "입출고내역"); + toast.success("다운로드 완료"); + }; + + // ════════ 렌더 ════════ + + return ( +
+ {/* 검색 */} +
+ + + + +
+ +
+
+ + {/* 데이터 테이블 */} +
+ {/* 헤더 */} +
+
+ + 입출고 내역 + {data.length}건 +
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+ ) : data.length === 0 ? ( +
+ + 조회된 데이터가 없습니다 +
+ ) : ( + + + + + toggleAll(!!c)} /> + + 입출고구분 + 카테고리 + 처리일자 + 창고 + 위치 + 품목코드 + 품목명 + 수량 + 단위 + 로트번호 + 참조번호 + 담당자 + + + + {groupedData.map((row, idx) => { + if (row._group) { + const expanded = expandedGroups.has(row._key); + return ( + toggleGroup(row._key)}> + +
+ {expanded ? : } + {row._key} + {row._count}건 +
+
+ + {fmtNum(row._totalQty)} + + +
+ ); + } + // 그룹 접힘 체크 + if (groupBy !== "none") { + let gk: string; + switch (groupBy) { + case "warehouse_code": gk = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": gk = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: gk = row[groupBy] || "미지정"; + } + if (!expandedGroups.has(gk)) return null; + } + + const isIn = row.transaction_type === "입고"; + const qty = Number(row.quantity) || 0; + const checked = checkedIds.has(row.id); + const info = itemMap[row.item_code]; + + return ( + + + toggleOne(row.id)} /> + + + + {isIn ? : } + {row.transaction_type} + + + {row.remark || "-"} + {fmtDate(row.transaction_date)} + {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} + {row.location_code || "-"} + {row.item_code || "-"} + {info?.item_name || "-"} + + {isIn ? "+" : ""}{fmtNum(qty)} + + {info?.unit || "-"} + {row.lot_number || "-"} + {row.reference_number || "-"} + {row.manager_name || userMap[row.writer] || row.writer || "-"} + + ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_8/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/inbound-outbound/page.tsx new file mode 100644 index 00000000..fed63cef --- /dev/null +++ b/frontend/app/(main)/COMPANY_8/logistics/inbound-outbound/page.tsx @@ -0,0 +1,379 @@ +"use client"; + +/** + * 입출고관리 — 하드코딩 페이지 + * + * inventory_history 테이블 기반 입고+출고 통합 조회 + * 그룹핑, 검색, 엑셀 다운로드 지원 + */ + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Download, Loader2, Inbox, ChevronDown, ChevronRight, + ArrowDownToLine, ArrowUpFromLine, Package, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; + +const HISTORY_TABLE = "inventory_history"; + +const fmtNum = (v: any) => { + const n = Number(v); + return isNaN(n) ? "0" : n.toLocaleString(); +}; + +const fmtDate = (v: any) => { + if (!v) return "-"; + const s = String(v); + const m = s.match(/(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); +}; + +export default function InboundOutboundPage() { + const { user } = useAuth(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [groupBy, setGroupBy] = useState("none"); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [checkedIds, setCheckedIds] = useState>(new Set()); + + // 품목명/단위 캐시 + const [itemMap, setItemMap] = useState>({}); + const [warehouseMap, setWarehouseMap] = useState>({}); + const [userMap, setUserMap] = useState>({}); + + // ════════ 데이터 로드 ════════ + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = []; + if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); + if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); + + for (const f of searchFilters) { + if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); + } + + const res = await apiClient.post(`/table-management/tables/${HISTORY_TABLE}/data`, { + page: 1, size: 1000, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + sort: { columnName: "transaction_date", order: "desc" }, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + + // 품목 정보 조회 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; + if (itemCodes.length > 0) { + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: itemCodes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + } + setItemMap(map); + } catch { /* skip */ } + } + + // 창고 정보 조회 + try { + const whRes = await apiClient.post(`/table-management/tables/warehouse_info/data`, { + page: 1, size: 100, autoFilter: true, + }); + const whs = whRes.data?.data?.data || whRes.data?.data?.rows || []; + const whMap: Record = {}; + for (const w of whs) whMap[w.warehouse_code] = w.warehouse_name || w.warehouse_code; + setWarehouseMap(whMap); + } catch { /* skip */ } + + // 사용자 정보 조회 (writer → user_name 변환) + const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))]; + if (writerIds.length > 0) { + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, size: 500, autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + const uMap: Record = {}; + for (const u of users) uMap[u.user_id] = u.user_name || u.user_id; + setUserMap(uMap); + } catch { /* skip */ } + } + } catch { + toast.error("입출고 내역 조회 실패"); + } finally { + setLoading(false); + } + }, [searchFilters, typeFilter, categoryFilter]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // ════════ 카테고리 목록 (remark에서 추출) ════════ + + const categoryOptions = useMemo(() => { + const set = new Set(); + data.forEach((r) => { if (r.remark) set.add(r.remark); }); + return Array.from(set).sort(); + }, [data]); + + // ════════ 그룹핑 ════════ + + const groupedData = useMemo(() => { + if (groupBy === "none") return data; + const groups = new Map(); + for (const row of data) { + let key: string; + switch (groupBy) { + case "transaction_type": key = row.transaction_type || "미지정"; break; + case "remark": key = row.remark || "미지정"; break; + case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: key = row[groupBy] || "미지정"; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + const result: any[] = []; + groups.forEach((items, gk) => { + const totalQty = items.reduce((s, r) => s + (Number(r.quantity) || 0), 0); + result.push({ _group: true, _key: gk, _count: items.length, _totalQty: totalQty }); + result.push(...items); + }); + if (expandedGroups.size === 0) setExpandedGroups(new Set(Array.from(groups.keys()))); + return result; + }, [data, groupBy, itemMap, warehouseMap]); + + const toggleGroup = (key: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // ════════ 체크박스 ════════ + + const allIds = data.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.has(id)); + const toggleAll = (checked: boolean) => setCheckedIds(checked ? new Set(allIds) : new Set()); + const toggleOne = (id: string) => { + setCheckedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + // ════════ 엑셀 ════════ + + const handleExcel = async () => { + if (data.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return; } + const rows = data.map((r, i) => ({ + No: i + 1, + 입출고구분: r.transaction_type || "", + 카테고리: r.remark || "", + 처리일자: fmtDate(r.transaction_date), + 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", + 위치: r.location_code || "", + 품목코드: r.item_code || "", + 품목명: itemMap[r.item_code]?.item_name || "", + 수량: Number(r.quantity) || 0, + 단위: itemMap[r.item_code]?.unit || "", + 로트번호: r.lot_number || "", + 참조번호: r.reference_number || "", + 담당자: r.manager_name || userMap[r.writer] || r.writer || "", + })); + const _n = new Date(); + await exportToExcel(rows, `입출고관리_${_n.getFullYear()}${String(_n.getMonth() + 1).padStart(2, "0")}${String(_n.getDate()).padStart(2, "0")}.xlsx`, "입출고내역"); + toast.success("다운로드 완료"); + }; + + // ════════ 렌더 ════════ + + return ( +
+ {/* 검색 */} +
+ + + + +
+ +
+
+ + {/* 데이터 테이블 */} +
+ {/* 헤더 */} +
+
+ + 입출고 내역 + {data.length}건 +
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+ ) : data.length === 0 ? ( +
+ + 조회된 데이터가 없습니다 +
+ ) : ( + + + + + toggleAll(!!c)} /> + + 입출고구분 + 카테고리 + 처리일자 + 창고 + 위치 + 품목코드 + 품목명 + 수량 + 단위 + 로트번호 + 참조번호 + 담당자 + + + + {groupedData.map((row, idx) => { + if (row._group) { + const expanded = expandedGroups.has(row._key); + return ( + toggleGroup(row._key)}> + +
+ {expanded ? : } + {row._key} + {row._count}건 +
+
+ + {fmtNum(row._totalQty)} + + +
+ ); + } + // 그룹 접힘 체크 + if (groupBy !== "none") { + let gk: string; + switch (groupBy) { + case "warehouse_code": gk = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": gk = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: gk = row[groupBy] || "미지정"; + } + if (!expandedGroups.has(gk)) return null; + } + + const isIn = row.transaction_type === "입고"; + const qty = Number(row.quantity) || 0; + const checked = checkedIds.has(row.id); + const info = itemMap[row.item_code]; + + return ( + + + toggleOne(row.id)} /> + + + + {isIn ? : } + {row.transaction_type} + + + {row.remark || "-"} + {fmtDate(row.transaction_date)} + {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} + {row.location_code || "-"} + {row.item_code || "-"} + {info?.item_name || "-"} + + {isIn ? "+" : ""}{fmtNum(qty)} + + {info?.unit || "-"} + {row.lot_number || "-"} + {row.reference_number || "-"} + {row.manager_name || userMap[row.writer] || row.writer || "-"} + + ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_9/logistics/inbound-outbound/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/inbound-outbound/page.tsx new file mode 100644 index 00000000..fed63cef --- /dev/null +++ b/frontend/app/(main)/COMPANY_9/logistics/inbound-outbound/page.tsx @@ -0,0 +1,379 @@ +"use client"; + +/** + * 입출고관리 — 하드코딩 페이지 + * + * inventory_history 테이블 기반 입고+출고 통합 조회 + * 그룹핑, 검색, 엑셀 다운로드 지원 + */ + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Download, Loader2, Inbox, ChevronDown, ChevronRight, + ArrowDownToLine, ArrowUpFromLine, Package, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; + +const HISTORY_TABLE = "inventory_history"; + +const fmtNum = (v: any) => { + const n = Number(v); + return isNaN(n) ? "0" : n.toLocaleString(); +}; + +const fmtDate = (v: any) => { + if (!v) return "-"; + const s = String(v); + const m = s.match(/(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10); +}; + +export default function InboundOutboundPage() { + const { user } = useAuth(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [groupBy, setGroupBy] = useState("none"); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [checkedIds, setCheckedIds] = useState>(new Set()); + + // 품목명/단위 캐시 + const [itemMap, setItemMap] = useState>({}); + const [warehouseMap, setWarehouseMap] = useState>({}); + const [userMap, setUserMap] = useState>({}); + + // ════════ 데이터 로드 ════════ + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = []; + if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter }); + if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter }); + + for (const f of searchFilters) { + if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); + } + + const res = await apiClient.post(`/table-management/tables/${HISTORY_TABLE}/data`, { + page: 1, size: 1000, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + sort: { columnName: "transaction_date", order: "desc" }, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + + // 품목 정보 조회 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))]; + if (itemCodes.length > 0) { + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: itemCodes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" }; + } + setItemMap(map); + } catch { /* skip */ } + } + + // 창고 정보 조회 + try { + const whRes = await apiClient.post(`/table-management/tables/warehouse_info/data`, { + page: 1, size: 100, autoFilter: true, + }); + const whs = whRes.data?.data?.data || whRes.data?.data?.rows || []; + const whMap: Record = {}; + for (const w of whs) whMap[w.warehouse_code] = w.warehouse_name || w.warehouse_code; + setWarehouseMap(whMap); + } catch { /* skip */ } + + // 사용자 정보 조회 (writer → user_name 변환) + const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))]; + if (writerIds.length > 0) { + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, size: 500, autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + const uMap: Record = {}; + for (const u of users) uMap[u.user_id] = u.user_name || u.user_id; + setUserMap(uMap); + } catch { /* skip */ } + } + } catch { + toast.error("입출고 내역 조회 실패"); + } finally { + setLoading(false); + } + }, [searchFilters, typeFilter, categoryFilter]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // ════════ 카테고리 목록 (remark에서 추출) ════════ + + const categoryOptions = useMemo(() => { + const set = new Set(); + data.forEach((r) => { if (r.remark) set.add(r.remark); }); + return Array.from(set).sort(); + }, [data]); + + // ════════ 그룹핑 ════════ + + const groupedData = useMemo(() => { + if (groupBy === "none") return data; + const groups = new Map(); + for (const row of data) { + let key: string; + switch (groupBy) { + case "transaction_type": key = row.transaction_type || "미지정"; break; + case "remark": key = row.remark || "미지정"; break; + case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: key = row[groupBy] || "미지정"; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + const result: any[] = []; + groups.forEach((items, gk) => { + const totalQty = items.reduce((s, r) => s + (Number(r.quantity) || 0), 0); + result.push({ _group: true, _key: gk, _count: items.length, _totalQty: totalQty }); + result.push(...items); + }); + if (expandedGroups.size === 0) setExpandedGroups(new Set(Array.from(groups.keys()))); + return result; + }, [data, groupBy, itemMap, warehouseMap]); + + const toggleGroup = (key: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + // ════════ 체크박스 ════════ + + const allIds = data.map((r) => r.id); + const allChecked = allIds.length > 0 && allIds.every((id) => checkedIds.has(id)); + const toggleAll = (checked: boolean) => setCheckedIds(checked ? new Set(allIds) : new Set()); + const toggleOne = (id: string) => { + setCheckedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + // ════════ 엑셀 ════════ + + const handleExcel = async () => { + if (data.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return; } + const rows = data.map((r, i) => ({ + No: i + 1, + 입출고구분: r.transaction_type || "", + 카테고리: r.remark || "", + 처리일자: fmtDate(r.transaction_date), + 창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "", + 위치: r.location_code || "", + 품목코드: r.item_code || "", + 품목명: itemMap[r.item_code]?.item_name || "", + 수량: Number(r.quantity) || 0, + 단위: itemMap[r.item_code]?.unit || "", + 로트번호: r.lot_number || "", + 참조번호: r.reference_number || "", + 담당자: r.manager_name || userMap[r.writer] || r.writer || "", + })); + const _n = new Date(); + await exportToExcel(rows, `입출고관리_${_n.getFullYear()}${String(_n.getMonth() + 1).padStart(2, "0")}${String(_n.getDate()).padStart(2, "0")}.xlsx`, "입출고내역"); + toast.success("다운로드 완료"); + }; + + // ════════ 렌더 ════════ + + return ( +
+ {/* 검색 */} +
+ + + + +
+ +
+
+ + {/* 데이터 테이블 */} +
+ {/* 헤더 */} +
+
+ + 입출고 내역 + {data.length}건 +
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+ ) : data.length === 0 ? ( +
+ + 조회된 데이터가 없습니다 +
+ ) : ( + + + + + toggleAll(!!c)} /> + + 입출고구분 + 카테고리 + 처리일자 + 창고 + 위치 + 품목코드 + 품목명 + 수량 + 단위 + 로트번호 + 참조번호 + 담당자 + + + + {groupedData.map((row, idx) => { + if (row._group) { + const expanded = expandedGroups.has(row._key); + return ( + toggleGroup(row._key)}> + +
+ {expanded ? : } + {row._key} + {row._count}건 +
+
+ + {fmtNum(row._totalQty)} + + +
+ ); + } + // 그룹 접힘 체크 + if (groupBy !== "none") { + let gk: string; + switch (groupBy) { + case "warehouse_code": gk = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break; + case "item_code": gk = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break; + default: gk = row[groupBy] || "미지정"; + } + if (!expandedGroups.has(gk)) return null; + } + + const isIn = row.transaction_type === "입고"; + const qty = Number(row.quantity) || 0; + const checked = checkedIds.has(row.id); + const info = itemMap[row.item_code]; + + return ( + + + toggleOne(row.id)} /> + + + + {isIn ? : } + {row.transaction_type} + + + {row.remark || "-"} + {fmtDate(row.transaction_date)} + {warehouseMap[row.warehouse_code] || row.warehouse_code || "-"} + {row.location_code || "-"} + {row.item_code || "-"} + {info?.item_name || "-"} + + {isIn ? "+" : ""}{fmtNum(qty)} + + {info?.unit || "-"} + {row.lot_number || "-"} + {row.reference_number || "-"} + {row.manager_name || userMap[row.writer] || row.writer || "-"} + + ); + })} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/components/layout/AdminPageRenderer.tsx b/frontend/components/layout/AdminPageRenderer.tsx index 0313e051..0fce1309 100644 --- a/frontend/components/layout/AdminPageRenderer.tsx +++ b/frontend/components/layout/AdminPageRenderer.tsx @@ -113,6 +113,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_7/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_7/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_7/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/logistics/info": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/info/page"), { ssr: false, loading: LoadingFallback }), @@ -158,6 +159,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_7/mold/info": dynamic(() => import("@/app/(main)/COMPANY_7/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_16/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/logistics/inventory": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/inventory/page"), { ssr: false, loading: LoadingFallback }), @@ -200,6 +202,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_8/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_8/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_8/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/inventory": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/inventory/page"), { ssr: false, loading: LoadingFallback }), @@ -242,6 +245,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_10/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_10/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_10/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/inventory": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/inventory/page"), { ssr: false, loading: LoadingFallback }), @@ -284,6 +288,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_29/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_29/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_29/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/inventory": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/inventory/page"), { ssr: false, loading: LoadingFallback }), @@ -326,6 +331,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_9/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_9/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_9/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/info": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/info/page"), { ssr: false, loading: LoadingFallback }), @@ -369,6 +375,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_30/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_30/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_30/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/info": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/info/page"), { ssr: false, loading: LoadingFallback }), @@ -486,6 +493,7 @@ const DYNAMIC_ADMIN_IMPORTS: Record Promise> = { "/COMPANY_7/equipment/plc-settings": () => import("@/app/(main)/COMPANY_7/equipment/plc-settings/page"), "/COMPANY_7/logistics/material-status": () => import("@/app/(main)/COMPANY_7/logistics/material-status/page"), "/COMPANY_7/logistics/outbound": () => import("@/app/(main)/COMPANY_7/logistics/outbound/page"), + "/COMPANY_7/logistics/inbound-outbound": () => import("@/app/(main)/COMPANY_7/logistics/inbound-outbound/page"), "/COMPANY_7/logistics/receiving": () => import("@/app/(main)/COMPANY_7/logistics/receiving/page"), "/COMPANY_7/logistics/packaging": () => import("@/app/(main)/COMPANY_7/logistics/packaging/page"), "/COMPANY_7/logistics/info": () => import("@/app/(main)/COMPANY_7/logistics/info/page"), @@ -514,6 +522,7 @@ const DYNAMIC_ADMIN_IMPORTS: Record Promise> = { "/COMPANY_10/equipment/info": () => import("@/app/(main)/COMPANY_10/equipment/info/page"), "/COMPANY_10/logistics/material-status": () => import("@/app/(main)/COMPANY_10/logistics/material-status/page"), "/COMPANY_10/logistics/outbound": () => import("@/app/(main)/COMPANY_10/logistics/outbound/page"), + "/COMPANY_10/logistics/inbound-outbound": () => import("@/app/(main)/COMPANY_10/logistics/inbound-outbound/page"), "/COMPANY_10/logistics/receiving": () => import("@/app/(main)/COMPANY_10/logistics/receiving/page"), "/COMPANY_10/logistics/packaging": () => import("@/app/(main)/COMPANY_10/logistics/packaging/page"), "/COMPANY_10/outsourcing/subcontractor": () => import("@/app/(main)/COMPANY_10/outsourcing/subcontractor/page"), @@ -547,6 +556,7 @@ const DYNAMIC_ADMIN_IMPORTS: Record Promise> = { "/COMPANY_9/monitoring/quality": () => import("@/app/(main)/COMPANY_9/monitoring/quality/page"), "/COMPANY_9/logistics/material-status": () => import("@/app/(main)/COMPANY_9/logistics/material-status/page"), "/COMPANY_9/logistics/outbound": () => import("@/app/(main)/COMPANY_9/logistics/outbound/page"), + "/COMPANY_9/logistics/inbound-outbound": () => import("@/app/(main)/COMPANY_9/logistics/inbound-outbound/page"), "/COMPANY_9/logistics/receiving": () => import("@/app/(main)/COMPANY_9/logistics/receiving/page"), "/COMPANY_9/logistics/packaging": () => import("@/app/(main)/COMPANY_9/logistics/packaging/page"), "/COMPANY_9/logistics/info": () => import("@/app/(main)/COMPANY_9/logistics/info/page"), @@ -589,6 +599,7 @@ const DYNAMIC_ADMIN_IMPORTS: Record Promise> = { "/COMPANY_30/monitoring/quality": () => import("@/app/(main)/COMPANY_30/monitoring/quality/page"), "/COMPANY_30/logistics/material-status": () => import("@/app/(main)/COMPANY_30/logistics/material-status/page"), "/COMPANY_30/logistics/outbound": () => import("@/app/(main)/COMPANY_30/logistics/outbound/page"), + "/COMPANY_30/logistics/inbound-outbound": () => import("@/app/(main)/COMPANY_30/logistics/inbound-outbound/page"), "/COMPANY_30/logistics/receiving": () => import("@/app/(main)/COMPANY_30/logistics/receiving/page"), "/COMPANY_30/logistics/packaging": () => import("@/app/(main)/COMPANY_30/logistics/packaging/page"), "/COMPANY_30/logistics/info": () => import("@/app/(main)/COMPANY_30/logistics/info/page"), @@ -623,6 +634,7 @@ const DYNAMIC_ADMIN_IMPORTS: Record Promise> = { "/COMPANY_29/equipment/info": () => import("@/app/(main)/COMPANY_29/equipment/info/page"), "/COMPANY_29/logistics/material-status": () => import("@/app/(main)/COMPANY_29/logistics/material-status/page"), "/COMPANY_29/logistics/outbound": () => import("@/app/(main)/COMPANY_29/logistics/outbound/page"), + "/COMPANY_29/logistics/inbound-outbound": () => import("@/app/(main)/COMPANY_29/logistics/inbound-outbound/page"), "/COMPANY_29/logistics/receiving": () => import("@/app/(main)/COMPANY_29/logistics/receiving/page"), "/COMPANY_29/logistics/packaging": () => import("@/app/(main)/COMPANY_29/logistics/packaging/page"), "/COMPANY_29/outsourcing/subcontractor": () => import("@/app/(main)/COMPANY_29/outsourcing/subcontractor/page"),