Enhance Outbound and Excel Service Functionality
- Added `inventory_unit` to the item selection query in the outbound controller to improve data retrieval. - Updated the multi-table Excel service to exclude overlapping headers between parent and child levels, ensuring accurate data insertion. - Introduced new category combobox components for better user interaction in the supplied item page. - Enhanced the inbound-outbound page to correctly map user IDs, including super admin handling for user information retrieval. (TASK: ERP-XXX)
This commit is contained in:
@@ -740,7 +740,7 @@ export async function getItems(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, item_number, item_name, size AS spec, material, unit,
|
||||
id, item_number, item_name, size AS spec, material, unit, inventory_unit,
|
||||
COALESCE(width::text, '') AS width,
|
||||
COALESCE(height::text, '') AS height,
|
||||
COALESCE(thickness::text, '') AS thickness,
|
||||
|
||||
@@ -156,6 +156,17 @@ class MultiTableExcelService {
|
||||
() => new Map()
|
||||
);
|
||||
|
||||
// 자식 단계 빈 데이터 판정에 부모와 겹치는 헤더는 제외해야 한다.
|
||||
// (예: customer_mng/customer_item_mapping 둘 다 '상태','비고' 헤더를 가지면
|
||||
// 부모용으로 입력한 값이 자식에도 잘못 들어가 자식 INSERT가 시도된다.)
|
||||
const ownHeadersPerLevel: Set<string>[] = activeLevels.map((lv, i) => {
|
||||
const headers = new Set(lv.columns.map((c) => c.excelHeader));
|
||||
for (let j = 0; j < i; j++) {
|
||||
for (const c of activeLevels[j].columns) headers.delete(c.excelHeader);
|
||||
}
|
||||
return headers;
|
||||
});
|
||||
|
||||
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
||||
const row = rows[rowIdx];
|
||||
|
||||
@@ -178,8 +189,19 @@ class MultiTableExcelService {
|
||||
}
|
||||
}
|
||||
|
||||
const hasAnyData = Object.keys(levelData).length > 0;
|
||||
if (!hasAnyData && lvlIdx > 0) {
|
||||
// 자식 레벨은 '부모와 겹치지 않는 고유 헤더'에 값이 있을 때만 INSERT 진행
|
||||
const ownHeaders = ownHeadersPerLevel[lvlIdx];
|
||||
const hasOwnData =
|
||||
lvlIdx === 0
|
||||
? Object.keys(levelData).length > 0
|
||||
: level.columns.some(
|
||||
(c) =>
|
||||
ownHeaders.has(c.excelHeader) &&
|
||||
levelData[c.dbColumn] !== undefined &&
|
||||
levelData[c.dbColumn] !== ""
|
||||
);
|
||||
|
||||
if (!hasOwnData && lvlIdx > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
2129
frontend/app/(main)/COMPANY_7/sales/supplied-item/page.tsx
Normal file
2129
frontend/app/(main)/COMPANY_7/sales/supplied-item/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -203,12 +203,25 @@ export default function InboundOutboundPage() {
|
||||
setLocationMap(locMap);
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 사용자 정보 조회 (writer → user_name 변환)
|
||||
const writerIds = [...new Set(rows.map((r: any) => r.writer).filter(Boolean))];
|
||||
// 사용자 정보 조회 (writer/manager_id → user_name 변환)
|
||||
// 회사 스코프 autoFilter를 해제하고 명시 ID 필터로 조회 → 슈퍼관리자(company_code='*', 예: wace) 매핑 누락 방지
|
||||
const writerIds = [
|
||||
...new Set(
|
||||
rows
|
||||
.flatMap((r: any) => [r.writer, r.manager_id])
|
||||
.filter((v: any) => typeof v === "string" && v.length > 0)
|
||||
),
|
||||
];
|
||||
if (writerIds.length > 0) {
|
||||
try {
|
||||
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
|
||||
page: 1, size: 0, autoFilter: true,
|
||||
page: 1,
|
||||
size: 0,
|
||||
autoFilter: { enabled: false }, // 회사 스코프 해제 (슈퍼관리자 포함)
|
||||
dataFilter: {
|
||||
enabled: true,
|
||||
filters: [{ columnName: "user_id", operator: "in", value: writerIds }],
|
||||
},
|
||||
});
|
||||
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
|
||||
const uMap: Record<string, string> = {};
|
||||
|
||||
@@ -276,7 +276,8 @@ export default function OutboundPage() {
|
||||
const [dnPreview, setDnPreview] = useState<DeliveryNoteWithDetails | null>(null); // PDF 미리보기 대상
|
||||
const [dnPrinting, setDnPrinting] = useState(false); // PDF 생성 중
|
||||
|
||||
// 카테고리 코드→라벨 매핑 (재질, 단위)
|
||||
// 카테고리 코드→라벨 매핑 (재질, 재고단위, 단위)
|
||||
// 출고 picker 단위는 재고단위(inventory_unit) 기준이 정본. 빈 값은 unit fallback.
|
||||
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
|
||||
useEffect(() => {
|
||||
const flatten = (arr: any[]): { code: string; label: string }[] => {
|
||||
@@ -289,7 +290,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
...["material", "inventory_unit", "unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_8`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -978,7 +979,8 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: (item as any).inventory_unit || "EA",
|
||||
// 재고단위(inventory_unit) 우선, 비었으면 기본단위(unit) fallback, 그래도 없으면 "EA"
|
||||
unit: item.inventory_unit || item.unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -2256,7 +2258,16 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material || "") || "-"}>{resolveCat("material", item.material || "") || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit || "") || "-"}</TableCell>
|
||||
<TableCell className="p-2">{
|
||||
// 재고단위(inventory_unit) 우선, 비었으면 기본단위(unit) fallback.
|
||||
// 매칭 실패 시 resolveCat 내부에서 코드 fallback (빈칸 금지).
|
||||
(() => {
|
||||
const invLabel = resolveCat("inventory_unit", item.inventory_unit || "");
|
||||
if (invLabel) return invLabel;
|
||||
const unitLabel = resolveCat("unit", item.unit || "");
|
||||
return unitLabel || "-";
|
||||
})()
|
||||
}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -897,6 +897,7 @@ export default function PurchaseOrderPage() {
|
||||
<DialogContent
|
||||
className="max-w-[95vw] w-[1200px]"
|
||||
style={{ maxHeight: "90vh", display: "flex", flexDirection: "column" }}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditMode ? (isReadOnly ? "발주 상세" : "발주 수정") : "발주 등록"}</DialogTitle>
|
||||
|
||||
@@ -99,6 +99,7 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/COMPANY_7/sales/order": dynamic(() => import("@/app/(main)/COMPANY_7/sales/order/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_7/sales/customer": dynamic(() => import("@/app/(main)/COMPANY_7/sales/customer/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_7/sales/sales-item": dynamic(() => import("@/app/(main)/COMPANY_7/sales/sales-item/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_7/sales/supplied-item": dynamic(() => import("@/app/(main)/COMPANY_7/sales/supplied-item/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_7/sales/shipping-order": dynamic(() => import("@/app/(main)/COMPANY_7/sales/shipping-order/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_7/sales/shipping-plan": dynamic(() => import("@/app/(main)/COMPANY_7/sales/shipping-plan/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_7/sales/claim": dynamic(() => import("@/app/(main)/COMPANY_7/sales/claim/page"), { ssr: false, loading: LoadingFallback }),
|
||||
@@ -588,6 +589,7 @@ const DYNAMIC_ADMIN_IMPORTS: Record<string, () => Promise<any>> = {
|
||||
"/COMPANY_7/sales/order": () => import("@/app/(main)/COMPANY_7/sales/order/page"),
|
||||
"/COMPANY_7/sales/customer": () => import("@/app/(main)/COMPANY_7/sales/customer/page"),
|
||||
"/COMPANY_7/sales/sales-item": () => import("@/app/(main)/COMPANY_7/sales/sales-item/page"),
|
||||
"/COMPANY_7/sales/supplied-item": () => import("@/app/(main)/COMPANY_7/sales/supplied-item/page"),
|
||||
"/COMPANY_7/sales/shipping-order": () => import("@/app/(main)/COMPANY_7/sales/shipping-order/page"),
|
||||
"/COMPANY_7/sales/shipping-plan": () => import("@/app/(main)/COMPANY_7/sales/shipping-plan/page"),
|
||||
"/COMPANY_7/sales/claim": () => import("@/app/(main)/COMPANY_7/sales/claim/page"),
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface ItemSource {
|
||||
spec: string | null;
|
||||
material: string | null;
|
||||
unit: string | null;
|
||||
inventory_unit?: string | null;
|
||||
standard_price: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user