{
const num = val.replace(/[^\d.-]/g, "");
if (!num) return "";
@@ -52,161 +42,256 @@ const formatNumber = (val: string) => {
};
const parseNumber = (val: string) => val.replace(/,/g, "");
-// 좌측: 수주 마스터 집계 목록
-const LEFT_COLUMNS: DataGridColumn[] = [
- { key: "order_no", label: "수주번호", width: "w-[120px]" },
- { key: "partner_name", label: "거래처", minWidth: "min-w-[100px]" },
- { key: "item_count", label: "품목수", width: "w-[60px]", align: "right" },
- { key: "total_qty", label: "총수량", width: "w-[80px]", formatNumber: true, align: "right" },
- { key: "total_ship_qty", label: "출하량", width: "w-[70px]", formatNumber: true, align: "right" },
- { key: "total_balance", label: "잔량", width: "w-[70px]", formatNumber: true, align: "right" },
- { key: "total_amount", label: "총금액", width: "w-[100px]", formatNumber: true, align: "right" },
- { key: "due_date", label: "납기일", width: "w-[100px]" },
- { key: "status", label: "상태", width: "w-[70px]" },
+// 플랫 테이블 컬럼 정의 (마스터+디테일 통합)
+const FLAT_COLUMNS = [
+ { key: "order_no", label: "수주번호", source: "master" },
+ { key: "partner_id", label: "거래처", source: "master" },
+ { key: "order_date", label: "수주일", source: "master" },
+ { key: "part_code", label: "품번", source: "detail" },
+ { key: "part_name", label: "품명", source: "detail" },
+ { key: "spec", label: "규격", source: "detail" },
+ { key: "unit", label: "단위", source: "detail" },
+ { key: "qty", label: "수량", source: "detail" },
+ { key: "ship_qty", label: "출하수량", source: "detail" },
+ { key: "balance_qty", label: "잔량", source: "detail" },
+ { key: "unit_price", label: "단가", source: "detail" },
+ { key: "amount", label: "금액", source: "detail" },
+ { key: "due_date", label: "납기일", source: "detail" },
+ { key: "memo", label: "메모", source: "master" },
];
-// 우측: 품목 상세 (가로/세로/두께/면적 포함)
-const RIGHT_COLUMNS: DataGridColumn[] = [
- { key: "division", label: "구분", width: "w-[70px]" },
- { key: "part_name", label: "품명", minWidth: "min-w-[120px]" },
- { key: "spec", label: "규격", width: "w-[100px]" },
- { key: "width", label: "가로", width: "w-[65px]", formatNumber: true, align: "right" },
- { key: "height", label: "세로", width: "w-[65px]", formatNumber: true, align: "right" },
- { key: "thickness", label: "두께", width: "w-[60px]", align: "right" },
- { key: "area", label: "면적", width: "w-[70px]", align: "right" },
- { key: "unit", label: "단위", width: "w-[50px]" },
- { key: "qty", label: "수량", width: "w-[70px]", formatNumber: true, align: "right" },
- { key: "ship_qty", label: "출하", width: "w-[60px]", formatNumber: true, align: "right" },
- { key: "balance_qty", label: "잔량", width: "w-[60px]", formatNumber: true, align: "right" },
- { key: "unit_price", label: "단가", width: "w-[85px]", formatNumber: true, align: "right" },
- { key: "amount", label: "금액", width: "w-[95px]", formatNumber: true, align: "right" },
- { key: "due_date", label: "납기일", width: "w-[100px]" },
- { key: "memo", label: "비고", width: "w-[80px]" },
-];
+const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail");
-export default function JeilGlassOrderPage() {
+// 필터용 전체 키
+const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label }));
+
+// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(14) = 15
+const TOTAL_COLS = 15;
+
+// 헤더 필터 Popover
+function HeaderFilterPopover({
+ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear,
+}: {
+ colKey: string;
+ colLabel: string;
+ uniqueValues: string[];
+ filterValues: Set
;
+ onToggle: (colKey: string, value: string) => void;
+ onClear: (colKey: string) => void;
+}) {
+ const [filterSearch, setFilterSearch] = useState("");
+ const hasFilter = filterValues.size > 0;
+ const filteredValues = uniqueValues.filter(
+ (v) => !filterSearch || v.toLowerCase().includes(filterSearch.toLowerCase())
+ );
+
+ return (
+
+
+
+
+ e.stopPropagation()}>
+
+
+ 필터: {colLabel}
+ {hasFilter && (
+
+ )}
+
+
+
+ setFilterSearch(e.target.value)}
+ placeholder="검색..."
+ className="h-7 text-xs pl-7"
+ />
+
+
+ {filteredValues.slice(0, 100).map((val) => {
+ const isSelected = filterValues.has(val);
+ return (
+
onToggle(colKey, val)}
+ >
+
+ {isSelected && }
+
+
{val || "(빈 값)"}
+
+ );
+ })}
+ {filteredValues.length > 100 && (
+
+ ...외 {filteredValues.length - 100}개
+
+ )}
+
+
+
+
+ );
+}
+
+export default function SalesOrderPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
-
- // 좌측: 수주 목록
- const [masterOrders, setMasterOrders] = useState([]);
- const [allDetails, setAllDetails] = useState([]);
+ const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
- const [searchFilters, setSearchFilters] = useState([]);
- const [selectedOrderNo, setSelectedOrderNo] = useState(null);
- const [checkedIds, setCheckedIds] = useState([]);
- // 우측: 디테일
- const [detailItems, setDetailItems] = useState([]);
- const [detailLoading, setDetailLoading] = useState(false);
+ // 검색 필터 (DynamicSearchFilter에서 관리)
+ const [searchFilters, setSearchFilters] = useState([]);
// 모달
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [saving, setSaving] = useState(false);
const [masterForm, setMasterForm] = useState>({});
- const [modalDetailRows, setModalDetailRows] = useState([]);
+ const [detailRows, setDetailRows] = useState([]);
+ const [allowPriceEdit, setAllowPriceEdit] = useState(true);
// 품목 선택 모달
const [itemSelectOpen, setItemSelectOpen] = useState(false);
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
const [itemSearchResults, setItemSearchResults] = useState([]);
const [itemSearchLoading, setItemSearchLoading] = useState(false);
- const [itemCheckedIds, setItemCheckedIds] = useState>(new Set());
+ const [itemSelectedMap, setItemSelectedMap] = useState