Merge remote-tracking branch 'origin/main' into hjjeong
# Conflicts: # frontend/components/common/CompactFilterBar.tsx
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
/**
|
||||
* 품질관리 — 4개 신규 메뉴 라우트.
|
||||
* 품질관리 — wace_plm 의 QualityController + quality.xml 을 1:1 이식.
|
||||
*
|
||||
* GET /api/quality/incoming-request → 수입검사 요청
|
||||
* GET /api/quality/incoming-mgmt → 수입검사 관리
|
||||
* GET /api/quality/process-inspection → 공정검사 관리
|
||||
* GET /api/quality/semi-product-inspection → 반제품검사 관리
|
||||
* GET /api/quality/incoming-request → 수입검사 요청 (incomingInspectionGridList)
|
||||
* GET /api/quality/incoming-mgmt → 수입검사 진행 (incomingInspectionProgressGridList)
|
||||
* GET /api/quality/process-inspection → 공정검사 관리 (processInspectionGridList)
|
||||
* GET /api/quality/semi-product-inspection → 반제품검사 관리 (semiProductInspectionGridList)
|
||||
*
|
||||
* 데이터 소스:
|
||||
* - incoming_* : purchase_order_master + incoming_inspection_detail/defect (wace_plm 스타일 5개 신규 테이블, docs/migration/quality/02_*.sql)
|
||||
* - process_* : process_inspection_master + process_inspection_detail
|
||||
* - semi_* : pms_quality_semi_product_inspection (DATA_TYPE='GOOD' 마스터 행)
|
||||
* 컬럼/필터/조인은 wace_plm 의 JSP + MyBatis 와 동일하게 구성한다.
|
||||
* 일부 조인 테이블(arrival_plan, inventory_mgmt_in 등)이 vexplor_rps 에 없을 수 있어
|
||||
* LEFT JOIN 으로만 묶고, 미존재 시 SQL 실패해도 빈 응답으로 fallback.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from "express";
|
||||
@@ -28,126 +27,146 @@ function emptyList(res: Response) {
|
||||
}
|
||||
|
||||
// ─── 1. 수입검사 요청 ──────────────────────────────────────────
|
||||
// 발주서 마스터 + 검사 디테일 LEFT JOIN. 디테일이 없으면 미요청 상태로 노출.
|
||||
// wace quality.xml getIncomingInspectionList — 발주서 기반.
|
||||
router.get("/incoming-request", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { project_no, partner_objid, request_user_id } = req.query as Record<string, string>;
|
||||
const q = req.query as Record<string, string>;
|
||||
|
||||
const where: string[] = ["1=1"];
|
||||
const params: any[] = [];
|
||||
if (project_no) {
|
||||
params.push(`%${project_no}%`);
|
||||
where.push(`COALESCE(cm.project_no, '') ILIKE $${params.length}`);
|
||||
}
|
||||
if (partner_objid) {
|
||||
params.push(partner_objid);
|
||||
where.push(`pom.partner_objid = $${params.length}`);
|
||||
}
|
||||
if (request_user_id) {
|
||||
params.push(request_user_id);
|
||||
where.push(`iid.request_user_id = $${params.length}`);
|
||||
const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); };
|
||||
|
||||
if (q.search_proposal_no) add("COALESCE(srm.request_mng_no, '') ILIKE ?", `%${q.search_proposal_no}%`);
|
||||
if (q.search_purchase_order_no) add("pom.purchase_order_no ILIKE ?", `%${q.search_purchase_order_no}%`);
|
||||
if (q.project_no) add("COALESCE(cm.project_no, '') ILIKE ?", `%${q.project_no}%`);
|
||||
if (q.search_part_no) add("COALESCE(pm.part_no, '') ILIKE ?", `%${q.search_part_no}%`);
|
||||
if (q.search_part_name) add("COALESCE(pm.part_name, '') ILIKE ?", `%${q.search_part_name}%`);
|
||||
if (q.search_partner) add("pom.partner_objid::text = ?", q.search_partner);
|
||||
if (q.search_product_cd) add("pom.product_code::text = ?", q.search_product_cd);
|
||||
if (q.search_inspection_yn) add("COALESCE(iid.inspection_yn, '미요청') = ?", q.search_inspection_yn);
|
||||
if (q.request_user_id) add("COALESCE(iid.request_user_id, pom.writer) = ?", q.request_user_id);
|
||||
if (q.request_start_date) add("COALESCE(NULLIF(iid.request_date, ''), TO_CHAR(pom.regdate, 'YYYY-MM-DD')) >= ?", q.request_start_date);
|
||||
if (q.request_end_date) add("COALESCE(NULLIF(iid.request_date, ''), TO_CHAR(pom.regdate, 'YYYY-MM-DD')) <= ?", q.request_end_date);
|
||||
if (q.search_request_status) {
|
||||
const s = q.search_request_status;
|
||||
if (s === "미요청") where.push("iid.objid IS NULL");
|
||||
else if (s === "요청중") where.push("iid.objid IS NOT NULL AND iid.inspection_date IS NULL");
|
||||
else if (s === "요청완료") where.push("iid.inspection_date IS NOT NULL");
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
pom.objid::text AS objid,
|
||||
pom.purchase_order_no AS purchase_order_no,
|
||||
COALESCE(srm.proposal_no, '') AS proposal_no,
|
||||
COALESCE(cm.project_no, '') AS project_no,
|
||||
COALESCE(ci.code_name, '') AS product_name,
|
||||
'' AS part_no,
|
||||
'' AS part_name,
|
||||
COALESCE(client.partner_name, '') AS partner_name,
|
||||
'-' AS delivery_status,
|
||||
TO_CHAR(COALESCE(iid.request_date, pom.regdate::date), 'YYYY-MM-DD') AS request_date,
|
||||
COALESCE(ui_req.user_name, ui_w.user_name, '') AS request_user_name,
|
||||
COALESCE(iid.inspection_yn, '-') AS inspection_yn,
|
||||
pom.objid::text AS objid,
|
||||
COALESCE(srm.request_mng_no, '') AS proposal_no,
|
||||
pom.purchase_order_no AS purchase_order_no,
|
||||
COALESCE(cm.project_no, '') AS project_no,
|
||||
COALESCE(ci.code_name, '') AS product_name,
|
||||
COALESCE(pm.part_no, '') AS part_no,
|
||||
COALESCE(pm.part_name, '') AS part_name,
|
||||
COALESCE(client.client_nm, '') AS partner_name,
|
||||
'-' AS delivery_status,
|
||||
COALESCE(NULLIF(iid.request_date, ''), TO_CHAR(pom.regdate, 'YYYY-MM-DD')) AS request_date,
|
||||
COALESCE(ui.user_name, '') AS request_user_name,
|
||||
COALESCE(iid.inspection_yn, '-') AS inspection_yn,
|
||||
CASE
|
||||
WHEN iid.objid IS NULL THEN '미요청'
|
||||
WHEN iid.inspection_date IS NOT NULL THEN '검사완료'
|
||||
WHEN iid.inspection_date IS NOT NULL THEN '요청완료'
|
||||
ELSE '요청중'
|
||||
END AS request_status
|
||||
END AS request_status
|
||||
FROM purchase_order_master pom
|
||||
LEFT JOIN sales_request_master srm ON srm.objid = pom.sales_request_objid
|
||||
LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_objid
|
||||
LEFT JOIN code_info ci ON ci.objid = pom.product_cd
|
||||
LEFT JOIN client_mng client ON client.objid = pom.partner_objid
|
||||
LEFT JOIN user_info ui_w ON ui_w.user_id = pom.writer
|
||||
LEFT JOIN sales_request_master srm ON srm.objid = pom.sales_request_objid
|
||||
LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_mgmt_objid
|
||||
LEFT JOIN code_info ci ON ci.code_value = pom.product_code
|
||||
LEFT JOIN client_mng client ON client.objid = pom.partner_objid
|
||||
-- purchase_order_master 에 part_objid 가 없어 part_mng 직접 조인 불가. part_no/name 은 빈값으로 표시.
|
||||
LEFT JOIN part_mng pm ON FALSE
|
||||
LEFT JOIN incoming_inspection_detail iid ON iid.purchase_order_master_objid = pom.objid::text
|
||||
LEFT JOIN user_info ui_req ON ui_req.user_id = iid.request_user_id
|
||||
LEFT JOIN user_info ui ON ui.user_id = COALESCE(iid.request_user_id, pom.writer)
|
||||
WHERE ${where.join(" AND ")}
|
||||
ORDER BY pom.regdate DESC NULLS LAST
|
||||
ORDER BY pom.regdate DESC NULLS LAST, pom.objid DESC
|
||||
LIMIT 500
|
||||
`;
|
||||
const r = await pool.query(sql, params);
|
||||
res.json({
|
||||
success: true,
|
||||
list: r.rows,
|
||||
pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 },
|
||||
});
|
||||
res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } });
|
||||
} catch (err: any) {
|
||||
console.warn("[quality/incoming-request] fallback empty:", err?.message);
|
||||
emptyList(res);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── 2. 수입검사 관리 ──────────────────────────────────────────
|
||||
// ─── 2. 수입검사 진행 ──────────────────────────────────────────
|
||||
// wace quality.xml getIncomingInspectionProgressList — INCOMING_INSPECTION_DETAIL 기반,
|
||||
// 검사된 행만 (INSPECTION_YN = '검사'). DEFECT 합계로 불량률 계산.
|
||||
router.get("/incoming-mgmt", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { project_no, partner_objid, inspector_id } = req.query as Record<string, string>;
|
||||
const q = req.query as Record<string, string>;
|
||||
|
||||
const where: string[] = ["iid.objid IS NOT NULL"];
|
||||
// wace_plm 원본은 inspection_yn='검사' 만 노출하지만, '스킵' 행도 진행 화면에 보이도록 완화.
|
||||
// 검사 완료만 좁히려면 search_inspection_status='완료' 사용.
|
||||
const where: string[] = ["1=1"];
|
||||
const params: any[] = [];
|
||||
if (project_no) {
|
||||
params.push(`%${project_no}%`);
|
||||
where.push(`COALESCE(cm.project_no, '') ILIKE $${params.length}`);
|
||||
}
|
||||
if (partner_objid) {
|
||||
params.push(partner_objid);
|
||||
where.push(`pom.partner_objid = $${params.length}`);
|
||||
}
|
||||
if (inspector_id) {
|
||||
params.push(inspector_id);
|
||||
where.push(`iid.inspector_id = $${params.length}`);
|
||||
const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); };
|
||||
|
||||
if (q.search_proposal_no) add("COALESCE(srm.request_mng_no, '') ILIKE ?", `%${q.search_proposal_no}%`);
|
||||
if (q.search_purchase_order_no) add("pom.purchase_order_no ILIKE ?", `%${q.search_purchase_order_no}%`);
|
||||
if (q.project_no) add("COALESCE(cm.project_no, '') ILIKE ?", `%${q.project_no}%`);
|
||||
if (q.search_part_no) add("COALESCE(pm.part_no, '') ILIKE ?", `%${q.search_part_no}%`);
|
||||
if (q.search_part_name) add("COALESCE(pm.part_name, '') ILIKE ?", `%${q.search_part_name}%`);
|
||||
if (q.search_partner) add("pom.partner_objid::text = ?", q.search_partner);
|
||||
if (q.search_product_cd) add("pom.product_code::text = ?", q.search_product_cd);
|
||||
if (q.inspector_id) add("iid.inspector_id = ?", q.inspector_id);
|
||||
if (q.inspection_start_date) add("COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) >= ?", q.inspection_start_date);
|
||||
if (q.inspection_end_date) add("COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) <= ?", q.inspection_end_date);
|
||||
if (q.search_inspection_status) {
|
||||
const s = q.search_inspection_status;
|
||||
if (s === "완료") where.push("iid.inspection_date IS NOT NULL AND COALESCE(iid.inspection_result, '') <> ''");
|
||||
else if (s === "진행중") where.push("iid.inspection_date IS NULL");
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
iid.objid::text AS objid,
|
||||
COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) AS inspection_date,
|
||||
COALESCE(ui_ins.user_name, '') AS inspector_name,
|
||||
COALESCE(srm.request_mng_no, '') AS proposal_no,
|
||||
pom.purchase_order_no AS purchase_order_no,
|
||||
COALESCE(cm.project_no, '') AS project_no,
|
||||
COALESCE(ci.code_name, '') AS product_name,
|
||||
'' AS part_no,
|
||||
'' AS part_name,
|
||||
COALESCE(client.partner_name, '') AS partner_name,
|
||||
TO_CHAR(iid.inspection_date, 'YYYY-MM-DD') AS inspection_date,
|
||||
COALESCE(ui.user_name, '') AS inspector_name,
|
||||
iid.inspection_qty AS total_qty,
|
||||
(iid.inspection_qty - COALESCE(iid.defect_qty, 0)) AS good_qty,
|
||||
iid.defect_qty AS bad_qty,
|
||||
COALESCE(iid.inspection_result, '') AS inspection_result,
|
||||
COALESCE(pm.product_name, '') AS model_name,
|
||||
COALESCE(pm.part_no, '') AS part_no,
|
||||
COALESCE(pm.part_name, '') AS part_name,
|
||||
COALESCE(client.client_nm, '') AS partner_name,
|
||||
TO_CHAR(iid.reg_date, 'YYYY-MM-DD') AS delivery_date,
|
||||
iid.inspection_qty AS delivery_qty,
|
||||
'-' AS delivery_status,
|
||||
iid.inspection_qty AS inspection_qty,
|
||||
COALESCE((SELECT SUM(d.defect_qty) FROM incoming_inspection_defect d WHERE d.inspection_detail_objid = iid.objid), 0) AS defect_qty_sum,
|
||||
CASE
|
||||
WHEN iid.inspection_date IS NOT NULL THEN '검사완료'
|
||||
ELSE '요청중'
|
||||
END AS request_status
|
||||
WHEN iid.inspection_qty IS NULL OR iid.inspection_qty = 0 THEN 0
|
||||
ELSE ROUND(COALESCE((SELECT SUM(d.defect_qty) FROM incoming_inspection_defect d WHERE d.inspection_detail_objid = iid.objid), 0) / iid.inspection_qty * 100, 2)
|
||||
END AS defect_rate,
|
||||
CASE
|
||||
WHEN iid.inspection_date IS NULL THEN '미검사'
|
||||
WHEN COALESCE(iid.inspection_result, '') = '' THEN '진행중'
|
||||
ELSE '완료'
|
||||
END AS inspection_result,
|
||||
0 AS inspection_file_cnt
|
||||
FROM incoming_inspection_detail iid
|
||||
LEFT JOIN purchase_order_master pom ON pom.objid::text = iid.purchase_order_master_objid
|
||||
LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_objid
|
||||
LEFT JOIN code_info ci ON ci.objid = pom.product_cd
|
||||
LEFT JOIN client_mng client ON client.objid = pom.partner_objid
|
||||
LEFT JOIN user_info ui ON ui.user_id = iid.inspector_id
|
||||
LEFT JOIN purchase_order_master pom ON pom.objid::text = iid.purchase_order_master_objid
|
||||
LEFT JOIN sales_request_master srm ON srm.objid = pom.sales_request_objid
|
||||
LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_mgmt_objid
|
||||
LEFT JOIN code_info ci ON ci.code_value = pom.product_code
|
||||
LEFT JOIN client_mng client ON client.objid = pom.partner_objid
|
||||
LEFT JOIN part_mng pm ON pm.objid = pom.part_objid
|
||||
LEFT JOIN user_info ui_ins ON ui_ins.user_id = iid.inspector_id
|
||||
WHERE ${where.join(" AND ")}
|
||||
ORDER BY iid.reg_date DESC
|
||||
ORDER BY COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) DESC NULLS LAST, iid.objid DESC
|
||||
LIMIT 500
|
||||
`;
|
||||
const r = await pool.query(sql, params);
|
||||
res.json({
|
||||
success: true,
|
||||
list: r.rows,
|
||||
pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 },
|
||||
});
|
||||
res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } });
|
||||
} catch (err: any) {
|
||||
console.warn("[quality/incoming-mgmt] fallback empty:", err?.message);
|
||||
emptyList(res);
|
||||
@@ -155,65 +174,62 @@ router.get("/incoming-mgmt", async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// ─── 3. 공정검사 관리 ──────────────────────────────────────────
|
||||
// 마스터별로 디테일 N건의 inspection_qty / defect_qty 합계를 집계해 1행 반환.
|
||||
// wace quality.xml getProcessInspectionList — 마스터별 디테일 SUM.
|
||||
router.get("/process-inspection", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { project_no, productType, part_name, inspector_id, from, to } = req.query as Record<string, string>;
|
||||
const q = req.query as Record<string, string>;
|
||||
|
||||
const where: string[] = ["1=1"];
|
||||
const params: any[] = [];
|
||||
if (project_no) {
|
||||
params.push(`%${project_no}%`);
|
||||
where.push(`EXISTS (SELECT 1 FROM project_mgmt pm WHERE pm.objid = pid.project_objid AND pm.project_no ILIKE $${params.length})`);
|
||||
}
|
||||
if (part_name) {
|
||||
params.push(`%${part_name}%`);
|
||||
where.push(`pid.part_name ILIKE $${params.length}`);
|
||||
}
|
||||
if (inspector_id) {
|
||||
params.push(inspector_id);
|
||||
where.push(`pim.inspector_id = $${params.length}`);
|
||||
}
|
||||
if (from) {
|
||||
params.push(from);
|
||||
where.push(`pim.inspection_date >= $${params.length}`);
|
||||
}
|
||||
if (to) {
|
||||
params.push(to);
|
||||
where.push(`pim.inspection_date <= $${params.length}`);
|
||||
}
|
||||
const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); };
|
||||
|
||||
if (q.search_project_no) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 LEFT JOIN project_mgmt pj ON pj.objid::text = NULLIF(pid2.project_objid, '') WHERE pid2.master_objid = pim.objid AND COALESCE(pj.project_no, '') ILIKE ?)", `%${q.search_project_no}%`);
|
||||
if (q.productType) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 LEFT JOIN project_mgmt pj ON pj.objid::text = NULLIF(pid2.project_objid, '') WHERE pid2.master_objid = pim.objid AND pj.product::text = ?)", q.productType);
|
||||
if (q.search_part_no) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.part_no ILIKE ?)", `%${q.search_part_no}%`);
|
||||
if (q.search_part_name) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.part_name ILIKE ?)", `%${q.search_part_name}%`);
|
||||
if (q.search_work_env_status) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.work_env_status = ?)", q.search_work_env_status);
|
||||
if (q.search_measuring_device) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.measuring_device = ?)", q.search_measuring_device);
|
||||
if (q.search_inspector) add("pim.inspector_id = ?", q.search_inspector);
|
||||
if (q.search_inspection_date_from) add("pim.inspection_date >= ?", q.search_inspection_date_from);
|
||||
if (q.search_inspection_date_to) add("pim.inspection_date <= ?", q.search_inspection_date_to);
|
||||
if (q.search_inspection_result) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.inspection_result = ?)", q.search_inspection_result);
|
||||
if (q.search_process_cd) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.process_cd = ?)", q.search_process_cd);
|
||||
|
||||
// pim.inspection_date 가 varchar 이고 빈 문자열인 경우가 많아 reg_date 로 fallback.
|
||||
// pid.project_objid 도 varchar 빈 문자열이 다수라 NULLIF + 안전 캐스트 필요.
|
||||
const sql = `
|
||||
SELECT
|
||||
pim.objid::text AS objid,
|
||||
TO_CHAR(pim.inspection_date, 'YYYY-MM-DD') AS inspection_date,
|
||||
COALESCE(ui.user_name, '') AS inspector_name,
|
||||
COALESCE(MIN(pm.project_no), '') AS project_no,
|
||||
'' AS product_name,
|
||||
COALESCE(NULLIF(pim.inspection_date, ''), TO_CHAR(pim.reg_date, 'YYYY-MM-DD')) AS inspection_date,
|
||||
COALESCE(ui_ins.user_name, ui_w.user_name, pim.writer, '') AS inspector_name,
|
||||
COALESCE(MIN(pj.project_no), '') AS project_no,
|
||||
COALESCE(MIN(ci.code_name), '') AS product_name,
|
||||
COALESCE(MIN(pid.part_no), '') AS part_no,
|
||||
COALESCE(MIN(pid.part_name), '') AS part_name,
|
||||
COALESCE(SUM(pid.inspection_qty), 0) AS inspection_qty,
|
||||
COALESCE(SUM(pid.defect_qty), 0) AS defect_qty,
|
||||
COALESCE(MAX(pid.work_env_status), '') AS work_env_status,
|
||||
COALESCE(MAX(pid.measuring_device), '') AS measuring_device,
|
||||
COALESCE(MAX(pid.inspection_result), '') AS inspection_result,
|
||||
0 AS file_count
|
||||
CASE
|
||||
WHEN SUM(pid.defect_qty) > 0 THEN 'NG'
|
||||
WHEN COUNT(pid.objid) > 0 THEN 'OK'
|
||||
ELSE ''
|
||||
END AS inspection_result,
|
||||
0 AS process_inspection_file_cnt
|
||||
FROM process_inspection_master pim
|
||||
LEFT JOIN process_inspection_detail pid ON pid.master_objid = pim.objid
|
||||
LEFT JOIN project_mgmt pm ON pm.objid = pid.project_objid
|
||||
LEFT JOIN user_info ui ON ui.user_id = pim.inspector_id
|
||||
LEFT JOIN project_mgmt pj ON pj.objid::text = NULLIF(pid.project_objid, '')
|
||||
LEFT JOIN code_info ci ON ci.objid::text = pj.product::text
|
||||
LEFT JOIN user_info ui_ins ON ui_ins.user_id = NULLIF(pim.inspector_id, '')
|
||||
LEFT JOIN user_info ui_w ON ui_w.user_id = pim.writer
|
||||
WHERE ${where.join(" AND ")}
|
||||
GROUP BY pim.objid, pim.inspection_date, ui.user_name
|
||||
ORDER BY pim.inspection_date DESC NULLS LAST, pim.objid DESC
|
||||
GROUP BY pim.objid, pim.inspection_date, pim.reg_date, pim.writer, ui_ins.user_name, ui_w.user_name
|
||||
ORDER BY COALESCE(NULLIF(pim.inspection_date, ''), TO_CHAR(pim.reg_date, 'YYYY-MM-DD')) DESC NULLS LAST, pim.objid DESC
|
||||
LIMIT 500
|
||||
`;
|
||||
const r = await pool.query(sql, params);
|
||||
res.json({
|
||||
success: true,
|
||||
list: r.rows,
|
||||
pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 },
|
||||
});
|
||||
res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } });
|
||||
} catch (err: any) {
|
||||
console.warn("[quality/process-inspection] fallback empty:", err?.message);
|
||||
emptyList(res);
|
||||
@@ -221,47 +237,35 @@ router.get("/process-inspection", async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// ─── 4. 반제품검사 관리 ────────────────────────────────────────
|
||||
// DATA_TYPE='GOOD' 행을 마스터로 보고, 동일 inspection_group_id 의 'DEFECT' 행을
|
||||
// SUM 으로 묶어 불량/재생/최종양품 수량을 집계.
|
||||
// wace quality.xml getSemiProductInspectionList — DATA_TYPE='GOOD' 마스터,
|
||||
// 동일 INSPECTION_GROUP_ID 의 'DEFECT' 행 SUM 으로 불량/재생/최종양품 산정.
|
||||
router.get("/semi-product-inspection", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { model_name, part_no, part_name, writer, from, to } = req.query as Record<string, string>;
|
||||
const q = req.query as Record<string, string>;
|
||||
|
||||
const where: string[] = ["g.data_type IS NULL OR g.data_type = 'GOOD'"];
|
||||
const where: string[] = ["g.data_type = 'GOOD'"];
|
||||
const params: any[] = [];
|
||||
if (model_name) {
|
||||
params.push(`%${model_name}%`);
|
||||
where.push(`g.model_name ILIKE $${params.length}`);
|
||||
}
|
||||
if (part_no) {
|
||||
params.push(`%${part_no}%`);
|
||||
where.push(`g.part_no ILIKE $${params.length}`);
|
||||
}
|
||||
if (part_name) {
|
||||
params.push(`%${part_name}%`);
|
||||
where.push(`g.part_name ILIKE $${params.length}`);
|
||||
}
|
||||
if (writer) {
|
||||
params.push(writer);
|
||||
where.push(`g.writer = $${params.length}`);
|
||||
}
|
||||
if (from) {
|
||||
params.push(from);
|
||||
where.push(`g.inspection_date >= $${params.length}`);
|
||||
}
|
||||
if (to) {
|
||||
params.push(to);
|
||||
where.push(`g.inspection_date <= $${params.length}`);
|
||||
}
|
||||
const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); };
|
||||
|
||||
if (q.search_model_name) add("COALESCE(g.model_name, '') ILIKE ?", `%${q.search_model_name}%`);
|
||||
if (q.search_work_order_no) add("COALESCE(g.work_order_no, '') ILIKE ?", `%${q.search_work_order_no}%`);
|
||||
if (q.search_part_no) add("COALESCE(g.part_no, '') ILIKE ?", `%${q.search_part_no}%`);
|
||||
if (q.search_part_name) add("COALESCE(g.part_name, '') ILIKE ?", `%${q.search_part_name}%`);
|
||||
// 반제품 inspection_date 는 date 타입 → reg_date 와 동일 도메인.
|
||||
if (q.inspection_start_date) add("COALESCE(g.inspection_date, g.reg_date::date) >= ?::date", q.inspection_start_date);
|
||||
if (q.inspection_end_date) add("COALESCE(g.inspection_date, g.reg_date::date) <= ?::date", q.inspection_end_date);
|
||||
if (q.search_writer) add("g.writer = ?", q.search_writer);
|
||||
if (q.search_defect_type) add("EXISTS (SELECT 1 FROM pms_quality_semi_product_inspection d2 WHERE d2.inspection_group_id = g.inspection_group_id AND d2.data_type = 'DEFECT' AND d2.defect_type = ?)", q.search_defect_type);
|
||||
if (q.search_responsible_dept) add("EXISTS (SELECT 1 FROM pms_quality_semi_product_inspection d2 WHERE d2.inspection_group_id = g.inspection_group_id AND d2.data_type = 'DEFECT' AND d2.responsible_dept = ?)", q.search_responsible_dept);
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
g.objid::text AS objid,
|
||||
TO_CHAR(g.inspection_date, 'YYYY-MM-DD') AS inspection_date,
|
||||
TO_CHAR(COALESCE(g.inspection_date, g.reg_date::date), 'YYYY-MM-DD') AS inspection_date,
|
||||
COALESCE(ui.user_name, g.writer, '') AS writer_name,
|
||||
COALESCE(g.model_name, '') AS model_name,
|
||||
COALESCE(g.product_type, '') AS product_type,
|
||||
COALESCE(g.model_name, '') AS model_name,
|
||||
COALESCE(g.work_order_no, '') AS work_order_no,
|
||||
COALESCE(g.part_no, '') AS part_no,
|
||||
COALESCE(g.part_name, '') AS part_name,
|
||||
@@ -270,7 +274,7 @@ router.get("/semi-product-inspection", async (req: Request, res: Response) => {
|
||||
COALESCE(d.defect_qty_sum, 0) AS defective_qty,
|
||||
CASE
|
||||
WHEN COALESCE(g.receipt_qty, 0) = 0 THEN 0
|
||||
ELSE ROUND( COALESCE(d.defect_qty_sum, 0) / g.receipt_qty * 100, 2 )
|
||||
ELSE ROUND(COALESCE(d.defect_qty_sum, 0) / g.receipt_qty * 100, 2)
|
||||
END AS defect_rate,
|
||||
COALESCE(d.regen_qty_sum, 0) AS regeneration_qty,
|
||||
COALESCE(g.good_qty, 0) + COALESCE(d.regen_qty_sum, 0) AS final_good_qty
|
||||
@@ -284,15 +288,11 @@ router.get("/semi-product-inspection", async (req: Request, res: Response) => {
|
||||
) d ON true
|
||||
LEFT JOIN user_info ui ON ui.user_id = g.writer
|
||||
WHERE ${where.join(" AND ")}
|
||||
ORDER BY g.inspection_date DESC NULLS LAST, g.objid DESC
|
||||
ORDER BY COALESCE(g.inspection_date, g.reg_date::date) DESC NULLS LAST, g.objid DESC
|
||||
LIMIT 500
|
||||
`;
|
||||
const r = await pool.query(sql, params);
|
||||
res.json({
|
||||
success: true,
|
||||
list: r.rows,
|
||||
pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 },
|
||||
});
|
||||
res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } });
|
||||
} catch (err: any) {
|
||||
console.warn("[quality/semi-product-inspection] fallback empty:", err?.message);
|
||||
emptyList(res);
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
-- 품질관리 — waceplm.esgrin.com 운영 DB(211.115.91.141:11133/waceplm) 스키마/데이터 동기화.
|
||||
-- 2단계 마이그레이션(02_*.sql)에서 만든 5개 wace_plm 스타일 테이블의 PK 타입이 bigserial 이었으나,
|
||||
-- 실제 운영 DB 는 varchar(50) 을 사용하므로 DROP 후 운영 스키마 1:1 로 재정합한다.
|
||||
-- 추가로 운영 환경에만 있는 customer_cs, pms_quality_ecr 도 신규 생성.
|
||||
|
||||
DROP TABLE IF EXISTS public.incoming_inspection_defect CASCADE;
|
||||
DROP TABLE IF EXISTS public.incoming_inspection_detail CASCADE;
|
||||
DROP TABLE IF EXISTS public.process_inspection_detail CASCADE;
|
||||
DROP TABLE IF EXISTS public.process_inspection_master CASCADE;
|
||||
DROP TABLE IF EXISTS public.pms_quality_semi_product_inspection CASCADE;
|
||||
|
||||
-- 고객 CS (waceplm 운영판)
|
||||
CREATE TABLE IF NOT EXISTS public.customer_cs (
|
||||
objid varchar(50) PRIMARY KEY,
|
||||
receipt_no varchar(50),
|
||||
receipt_date varchar(10),
|
||||
qty numeric,
|
||||
customer_objid varchar(200),
|
||||
model_name varchar(200),
|
||||
product_name varchar(200),
|
||||
part_no varchar(200),
|
||||
production_date varchar(10),
|
||||
sales_date varchar(10),
|
||||
serial_no varchar(200),
|
||||
manufacturer varchar(200),
|
||||
complaint_content varchar(2000),
|
||||
action_content varchar(2000),
|
||||
blame_decision varchar(50),
|
||||
status varchar(50),
|
||||
remark varchar(2000),
|
||||
action_date varchar(10),
|
||||
action_user_id varchar(50),
|
||||
attach_file_objid varchar(50),
|
||||
writer varchar(50),
|
||||
reg_date timestamp,
|
||||
mod_date timestamp,
|
||||
part_name varchar,
|
||||
product_no varchar,
|
||||
action_type varchar,
|
||||
receipt_user_id varchar(50)
|
||||
);
|
||||
|
||||
-- ECR (waceplm 운영 신규 테이블 — 기존 ecr_mng 와는 별도 보존)
|
||||
CREATE TABLE IF NOT EXISTS public.pms_quality_ecr (
|
||||
objid varchar(50) PRIMARY KEY,
|
||||
ecr_no varchar(50),
|
||||
request_date varchar(10),
|
||||
requester_id varchar(50),
|
||||
part_no varchar(100),
|
||||
part_name varchar(200),
|
||||
issue_content text,
|
||||
due_date varchar(10),
|
||||
action_dept varchar(50),
|
||||
action_manager_id varchar(50),
|
||||
action_user_id varchar(50),
|
||||
action_content text,
|
||||
complete_date varchar(10),
|
||||
attach_file_objid varchar(50),
|
||||
remark text,
|
||||
writer varchar(50),
|
||||
reg_date timestamp,
|
||||
modifier varchar(50),
|
||||
mod_date timestamp,
|
||||
part_objid varchar,
|
||||
ecr_doc_summary text,
|
||||
ecr_doc_reason text,
|
||||
ecr_rev_no varchar(50),
|
||||
ecr_rev_date varchar(20),
|
||||
ecr_doc_form_no varchar(50),
|
||||
ecr_doc_author varchar(100),
|
||||
change_type varchar
|
||||
);
|
||||
|
||||
-- 수입검사 진행 상세
|
||||
CREATE TABLE IF NOT EXISTS public.incoming_inspection_detail (
|
||||
objid varchar(50) PRIMARY KEY,
|
||||
inventory_in_objid varchar(50),
|
||||
purchase_order_master_objid varchar,
|
||||
request_date varchar(10),
|
||||
request_user_id varchar(50),
|
||||
inspection_date varchar(10),
|
||||
inspector_id varchar(50),
|
||||
inspection_type varchar(50),
|
||||
inspection_yn varchar(10),
|
||||
defect_type varchar(50),
|
||||
defect_reason varchar(200),
|
||||
action_status varchar(50),
|
||||
inspection_qty numeric,
|
||||
defect_qty numeric,
|
||||
inspection_result varchar(50),
|
||||
attach_file_objid varchar(50),
|
||||
remark varchar(2000),
|
||||
writer varchar(50),
|
||||
reg_date timestamp
|
||||
);
|
||||
|
||||
-- 수입검사 불량 상세
|
||||
CREATE TABLE IF NOT EXISTS public.incoming_inspection_defect (
|
||||
objid varchar(50) PRIMARY KEY,
|
||||
inspection_detail_objid varchar(50),
|
||||
inspection_type varchar(50),
|
||||
inspection_date varchar(10),
|
||||
inspector_id varchar(50),
|
||||
defect_type varchar(50),
|
||||
defect_reason varchar(200),
|
||||
action_status varchar(50),
|
||||
action_result varchar(200),
|
||||
inspection_qty numeric,
|
||||
defect_qty numeric,
|
||||
inspection_result varchar(50),
|
||||
remark varchar(2000),
|
||||
writer varchar(50),
|
||||
reg_date timestamp
|
||||
);
|
||||
|
||||
-- 공정검사 마스터/디테일
|
||||
CREATE TABLE IF NOT EXISTS public.process_inspection_master (
|
||||
objid varchar(50) PRIMARY KEY,
|
||||
inspection_date varchar(10),
|
||||
inspector_id varchar(50),
|
||||
remark varchar(2000),
|
||||
writer varchar(50),
|
||||
reg_date timestamp,
|
||||
mod_date timestamp
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.process_inspection_detail (
|
||||
objid varchar(50) PRIMARY KEY,
|
||||
master_objid varchar(50),
|
||||
process_cd varchar(50),
|
||||
project_objid varchar(50),
|
||||
part_objid varchar(50),
|
||||
part_no varchar(200),
|
||||
part_name varchar(200),
|
||||
inspection_qty numeric,
|
||||
defect_qty numeric,
|
||||
work_env_status varchar(10),
|
||||
measuring_device varchar(10),
|
||||
dept_cd varchar(50),
|
||||
user_id varchar(50),
|
||||
inspection_date varchar(10),
|
||||
inspector_id varchar(50),
|
||||
remark varchar(2000),
|
||||
action_status varchar(50),
|
||||
inspection_result varchar(10),
|
||||
writer varchar(50),
|
||||
reg_date timestamp,
|
||||
mod_date timestamp
|
||||
);
|
||||
|
||||
-- 반제품검사 (GOOD/DEFECT 통합)
|
||||
CREATE TABLE IF NOT EXISTS public.pms_quality_semi_product_inspection (
|
||||
objid varchar(50) PRIMARY KEY,
|
||||
project_no varchar(200),
|
||||
work_order_no varchar(100),
|
||||
part_no varchar(200),
|
||||
part_name varchar(200),
|
||||
receipt_qty numeric,
|
||||
good_qty numeric,
|
||||
defect_qty numeric,
|
||||
disposition_type varchar(50),
|
||||
remark varchar(2000),
|
||||
writer varchar(50),
|
||||
reg_date timestamp,
|
||||
inspection_group_id varchar(50),
|
||||
data_type varchar(20),
|
||||
defect_type varchar(50),
|
||||
defect_cause varchar(200),
|
||||
responsible_dept varchar(50),
|
||||
worker varchar(50),
|
||||
process_status varchar(50),
|
||||
inspection_date varchar(10),
|
||||
inspector varchar(50),
|
||||
model_name varchar(200),
|
||||
product_type varchar(50),
|
||||
is_locked varchar(1)
|
||||
);
|
||||
|
||||
-- 데이터 복사 (운영 → 로컬, ON CONFLICT DO NOTHING). 실제 실행은 node script 로 진행.
|
||||
-- customer_cs 24 rows
|
||||
-- pms_quality_ecr 1
|
||||
-- process_inspection_master 3
|
||||
-- process_inspection_detail 13
|
||||
-- incoming_inspection_detail 1
|
||||
-- pms_quality_semi_product_inspection 84
|
||||
@@ -1,38 +1,47 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 수입검사 관리 — wace_plm incomingInspectionProgressList.jsp 이식.
|
||||
*
|
||||
* 수입검사 요청 기반에 검사자/검사일/검사결과/불량수량 컬럼이 추가됨.
|
||||
* 수입검사 진행 — wace_plm 의 incomingInspectionProgressList.jsp 1:1 이식.
|
||||
* 필터 11종, 그리드 19컬럼 (불량률 자동산정, 검사현황 컬러표시).
|
||||
*/
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
||||
import { PageHeader } from "@/components/common/PageHeader";
|
||||
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
|
||||
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
|
||||
import { qualityApi, IncomingMgmtRow } from "@/lib/api/quality";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// wace_plm incomingInspectionProgressList.jsp 그리드 1:1
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "purchase_order_no", label: "발주서 No", width: "w-[150px]", frozen: true },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[150px]" },
|
||||
{ key: "product_name", label: "제품구분", width: "w-[110px]", align: "center" },
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]" },
|
||||
{ key: "part_name", label: "품명", width: "w-[200px]" },
|
||||
{ key: "partner_name", label: "공급업체", width: "w-[160px]" },
|
||||
{ key: "inspection_date", label: "검사일", width: "w-[115px]", align: "center" },
|
||||
{ key: "inspector_name", label: "검사자", width: "w-[110px]", align: "center" },
|
||||
{ key: "total_qty", label: "검사수량", width: "w-[110px]", align: "right", formatNumber: true },
|
||||
{ key: "good_qty", label: "양품수량", width: "w-[110px]", align: "right", formatNumber: true },
|
||||
{ key: "bad_qty", label: "불량수량", width: "w-[110px]", align: "right", formatNumber: true },
|
||||
{ key: "inspection_result", label: "검사결과", width: "w-[100px]", align: "center" },
|
||||
{ key: "request_status", label: "요청현황", width: "w-[110px]", align: "center" },
|
||||
{ key: "inspection_date", label: "검사일", width: "w-[110px]", align: "center", frozen: true },
|
||||
{ key: "inspector_name", label: "검사자", width: "w-[90px]", align: "center" },
|
||||
{ key: "proposal_no", label: "품의서 No", width: "w-[120px]", align: "center" },
|
||||
{ key: "purchase_order_no", label: "발주서 No", width: "w-[120px]", align: "center" },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[140px]", align: "center" },
|
||||
{ key: "product_name", label: "제품구분", width: "w-[100px]", align: "center" },
|
||||
{ key: "model_name", label: "품명(모델명)", width: "w-[150px]" },
|
||||
{ key: "part_no", label: "부품품번", width: "w-[140px]" },
|
||||
{ key: "part_name", label: "부품명", width: "w-[150px]" },
|
||||
{ key: "partner_name", label: "공급업체", width: "w-[150px]" },
|
||||
{ key: "delivery_date", label: "입고일", width: "w-[110px]", align: "center" },
|
||||
{ key: "delivery_qty", label: "입고수량", width: "w-[100px]", align: "right", formatNumber: true },
|
||||
{ key: "delivery_status", label: "입고결과", width: "w-[90px]", align: "center" },
|
||||
{ key: "inspection_qty", label: "검사수량", width: "w-[100px]", align: "right", formatNumber: true },
|
||||
{ key: "defect_qty_sum", label: "불량수량", width: "w-[100px]", align: "right", formatNumber: true },
|
||||
{ key: "defect_rate", label: "불량률(%)", width: "w-[100px]", align: "right" },
|
||||
{ key: "inspection_result", label: "검사현황", width: "w-[100px]", align: "center" },
|
||||
{ key: "inspection_file_cnt", label: "검사성적서", width: "w-[100px]", align: "center", renderType: "clip" },
|
||||
];
|
||||
|
||||
const INSPECTION_STATUS_OPTIONS = ["전체", "완료", "진행중"];
|
||||
const DELIVERY_STATUS_OPTIONS = ["전체", "입고중", "입고완료", "지연"];
|
||||
|
||||
export default function IncomingMgmtPage() {
|
||||
const { user } = useAuth();
|
||||
const [rows, setRows] = useState<IncomingMgmtRow[]>([]);
|
||||
@@ -40,30 +49,58 @@ export default function IncomingMgmtPage() {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const [search, setSearch] = useState({
|
||||
search_proposal_no: "",
|
||||
search_purchase_order_no: "",
|
||||
project_no: "",
|
||||
partner_objid: "",
|
||||
search_part_no: "",
|
||||
search_part_name: "",
|
||||
search_partner: "",
|
||||
search_delivery_status: "",
|
||||
search_product_cd: "",
|
||||
inspector_id: "",
|
||||
inspection_start_date: "",
|
||||
inspection_end_date: "",
|
||||
search_inspection_status: "",
|
||||
});
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
if (!user) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = {};
|
||||
Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; });
|
||||
const res = await qualityApi.incomingMgmt(params);
|
||||
const res = await qualityApi.incomingMgmt(search);
|
||||
setRows(res.list.map((r) => ({ ...r, id: r.objid } as any)));
|
||||
setSelectedId(null);
|
||||
} catch {
|
||||
toast.error("수입검사 관리 목록 조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch { toast.error("수입검사 진행 목록 조회 실패"); }
|
||||
finally { setLoading(false); }
|
||||
}, [user, search]);
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
const handleReset = () => setSearch({ project_no: "", partner_objid: "", inspector_id: "" });
|
||||
const handleReset = () => setSearch({
|
||||
search_proposal_no: "", search_purchase_order_no: "", project_no: "",
|
||||
search_part_no: "", search_part_name: "", search_partner: "",
|
||||
search_delivery_status: "", search_product_cd: "",
|
||||
inspector_id: "", inspection_start_date: "", inspection_end_date: "",
|
||||
search_inspection_status: "",
|
||||
});
|
||||
|
||||
const summary = (() => {
|
||||
const fmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "총 입고수량", value: fmt(rows.reduce((a, r) => a + Number(r.delivery_qty || 0), 0)) },
|
||||
{ label: "총 검사수량", value: fmt(rows.reduce((a, r) => a + Number(r.inspection_qty || 0), 0)) },
|
||||
{ label: "총 불량수량", value: fmt(rows.reduce((a, r) => a + Number(r.defect_qty_sum || 0), 0)) },
|
||||
];
|
||||
})();
|
||||
|
||||
const HardcodedSelect = ({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: string[] }) => (
|
||||
<Select value={value || "all"} onValueChange={(v) => onChange(v === "all" || v === "전체" ? "" : v)}>
|
||||
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="전체" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((o) => <SelectItem key={o} value={o === "전체" ? "all" : o}>{o}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
@@ -73,22 +110,46 @@ export default function IncomingMgmtPage() {
|
||||
onReset={handleReset}
|
||||
actions={
|
||||
<Button size="sm" className="h-8 gap-1 text-xs" disabled>
|
||||
<Plus className="h-3.5 w-3.5" />수입검사 진행등록
|
||||
<Plus className="h-3.5 w-3.5" />수입검사 진행
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="프로젝트번호" width={160}>
|
||||
<Input value={search.project_no}
|
||||
onChange={(e) => setSearch({ ...search, project_no: e.target.value })} />
|
||||
<CompactFilterField label="품의서 No" width={130}>
|
||||
<Input value={search.search_proposal_no} onChange={(e) => setSearch({ ...search, search_proposal_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="공급업체 ID" width={160}>
|
||||
<Input value={search.partner_objid}
|
||||
onChange={(e) => setSearch({ ...search, partner_objid: e.target.value })} />
|
||||
<CompactFilterField label="발주서 No" width={130}>
|
||||
<Input value={search.search_purchase_order_no} onChange={(e) => setSearch({ ...search, search_purchase_order_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사자 ID" width={140}>
|
||||
<Input value={search.inspector_id}
|
||||
onChange={(e) => setSearch({ ...search, inspector_id: e.target.value })} />
|
||||
<CompactFilterField label="프로젝트번호" width={150}>
|
||||
<Input value={search.project_no} onChange={(e) => setSearch({ ...search, project_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="품번" width={140}>
|
||||
<Input value={search.search_part_no} onChange={(e) => setSearch({ ...search, search_part_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="품명" width={160}>
|
||||
<Input value={search.search_part_name} onChange={(e) => setSearch({ ...search, search_part_name: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="공급업체 ID" width={140}>
|
||||
<Input value={search.search_partner} onChange={(e) => setSearch({ ...search, search_partner: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="입고결과" width={120}>
|
||||
<HardcodedSelect value={search.search_delivery_status} onChange={(v) => setSearch({ ...search, search_delivery_status: v })} options={DELIVERY_STATUS_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="제품구분 ID" width={120}>
|
||||
<Input value={search.search_product_cd} onChange={(e) => setSearch({ ...search, search_product_cd: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사자 ID" width={120}>
|
||||
<Input value={search.inspector_id} onChange={(e) => setSearch({ ...search, inspector_id: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사일" width={280}>
|
||||
<CompactDateRange
|
||||
from={search.inspection_start_date} setFrom={(v) => setSearch({ ...search, inspection_start_date: v })}
|
||||
to={search.inspection_end_date} setTo={(v) => setSearch({ ...search, inspection_end_date: v })}
|
||||
/>
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사현황" width={110}>
|
||||
<HardcodedSelect value={search.search_inspection_status} onChange={(v) => setSearch({ ...search, search_inspection_status: v })} options={INSPECTION_STATUS_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
<DataGrid
|
||||
@@ -102,6 +163,7 @@ export default function IncomingMgmtPage() {
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
emptyMessage="조회된 수입검사 진행 내역이 없습니다."
|
||||
summaryStats={summary}
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
|
||||
@@ -1,38 +1,43 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 수입검사 요청 — wace_plm incomingInspectionList.jsp + QualityController.getIncomingInspectionList 이식.
|
||||
*
|
||||
* 그리드 컬럼은 wace_plm 원본의 12개를 1:1로 따른다.
|
||||
* 일부 백엔드 테이블 부재로 데이터가 비어 있을 수 있다(qualityRoutes.ts 참조).
|
||||
* 수입검사 요청 — wace_plm 의 incomingInspectionList.jsp 1:1 이식.
|
||||
* 필터 12종, 그리드 12컬럼.
|
||||
*/
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
||||
import { PageHeader } from "@/components/common/PageHeader";
|
||||
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
|
||||
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
|
||||
import { qualityApi, IncomingRequestRow } from "@/lib/api/quality";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// wace_plm incomingInspectionList.jsp 그리드 1:1 (12 컬럼)
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "purchase_order_no", label: "발주서 No", width: "w-[150px]", frozen: true },
|
||||
{ key: "proposal_no", label: "품의서 No", width: "w-[140px]" },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[150px]" },
|
||||
{ key: "product_name", label: "제품구분", width: "w-[110px]", align: "center" },
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]" },
|
||||
{ key: "part_name", label: "품명", width: "w-[200px]" },
|
||||
{ key: "partner_name", label: "공급업체", width: "w-[160px]" },
|
||||
{ key: "delivery_status", label: "입고결과", width: "w-[110px]", align: "center" },
|
||||
{ key: "request_date", label: "요청일", width: "w-[115px]", align: "center" },
|
||||
{ key: "request_user_name", label: "요청자", width: "w-[110px]", align: "center" },
|
||||
{ key: "inspection_yn", label: "검사여부", width: "w-[100px]", align: "center" },
|
||||
{ key: "request_status", label: "요청현황", width: "w-[110px]", align: "center" },
|
||||
{ key: "proposal_no", label: "품의서 No", width: "w-[130px]", align: "center", frozen: true },
|
||||
{ key: "purchase_order_no", label: "발주서 No", width: "w-[130px]", align: "center" },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[140px]", align: "center" },
|
||||
{ key: "product_name", label: "제품구분", width: "w-[110px]", align: "center" },
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]" },
|
||||
{ key: "part_name", label: "품명", width: "w-[180px]" },
|
||||
{ key: "partner_name", label: "공급업체", width: "w-[160px]" },
|
||||
{ key: "delivery_status", label: "입고결과", width: "w-[100px]", align: "center" },
|
||||
{ key: "request_date", label: "요청일", width: "w-[115px]", align: "center" },
|
||||
{ key: "request_user_name", label: "요청자", width: "w-[100px]", align: "center" },
|
||||
{ key: "inspection_yn", label: "검사여부", width: "w-[100px]", align: "center" },
|
||||
{ key: "request_status", label: "요청현황", width: "w-[110px]", align: "center" },
|
||||
];
|
||||
|
||||
// 하드코딩 옵션 (wace_plm 동일)
|
||||
const DELIVERY_STATUS_OPTIONS = ["전체", "입고중", "입고완료", "지연"];
|
||||
const INSPECTION_YN_OPTIONS = ["전체", "검사", "스킵"];
|
||||
const REQUEST_STATUS_OPTIONS = ["전체", "미요청", "요청중", "요청완료"];
|
||||
|
||||
export default function IncomingRequestPage() {
|
||||
const { user } = useAuth();
|
||||
const [rows, setRows] = useState<IncomingRequestRow[]>([]);
|
||||
@@ -40,30 +45,51 @@ export default function IncomingRequestPage() {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const [search, setSearch] = useState({
|
||||
search_proposal_no: "",
|
||||
search_purchase_order_no: "",
|
||||
project_no: "",
|
||||
partner_objid: "",
|
||||
search_part_no: "",
|
||||
search_part_name: "",
|
||||
search_partner: "",
|
||||
search_delivery_status: "",
|
||||
search_product_cd: "",
|
||||
search_inspection_yn: "",
|
||||
search_request_status: "",
|
||||
request_user_id: "",
|
||||
request_start_date: "",
|
||||
request_end_date: "",
|
||||
});
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
if (!user) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = {};
|
||||
Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; });
|
||||
const res = await qualityApi.incomingRequest(params);
|
||||
const res = await qualityApi.incomingRequest(search);
|
||||
setRows(res.list.map((r) => ({ ...r, id: r.objid } as any)));
|
||||
setSelectedId(null);
|
||||
} catch (e: any) {
|
||||
} catch {
|
||||
toast.error("수입검사 요청 목록 조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} finally { setLoading(false); }
|
||||
}, [user, search]);
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
const handleReset = () => setSearch({ project_no: "", partner_objid: "", request_user_id: "" });
|
||||
const handleReset = () => setSearch({
|
||||
search_proposal_no: "", search_purchase_order_no: "", project_no: "",
|
||||
search_part_no: "", search_part_name: "", search_partner: "",
|
||||
search_delivery_status: "", search_product_cd: "",
|
||||
search_inspection_yn: "", search_request_status: "",
|
||||
request_user_id: "", request_start_date: "", request_end_date: "",
|
||||
});
|
||||
|
||||
const HardcodedSelect = ({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: string[] }) => (
|
||||
<Select value={value || "all"} onValueChange={(v) => onChange(v === "all" || v === "전체" ? "" : v)}>
|
||||
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="전체" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((o) => <SelectItem key={o} value={o === "전체" ? "all" : o}>{o}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
@@ -78,17 +104,44 @@ export default function IncomingRequestPage() {
|
||||
}
|
||||
/>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="프로젝트번호" width={160}>
|
||||
<Input value={search.project_no}
|
||||
onChange={(e) => setSearch({ ...search, project_no: e.target.value })} />
|
||||
<CompactFilterField label="품의서 No" width={130}>
|
||||
<Input value={search.search_proposal_no} onChange={(e) => setSearch({ ...search, search_proposal_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="공급업체 ID" width={160}>
|
||||
<Input value={search.partner_objid}
|
||||
onChange={(e) => setSearch({ ...search, partner_objid: e.target.value })} />
|
||||
<CompactFilterField label="발주서 No" width={130}>
|
||||
<Input value={search.search_purchase_order_no} onChange={(e) => setSearch({ ...search, search_purchase_order_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="요청자 ID" width={140}>
|
||||
<Input value={search.request_user_id}
|
||||
onChange={(e) => setSearch({ ...search, request_user_id: e.target.value })} />
|
||||
<CompactFilterField label="프로젝트번호" width={150}>
|
||||
<Input value={search.project_no} onChange={(e) => setSearch({ ...search, project_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="품번" width={140}>
|
||||
<Input value={search.search_part_no} onChange={(e) => setSearch({ ...search, search_part_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="품명" width={160}>
|
||||
<Input value={search.search_part_name} onChange={(e) => setSearch({ ...search, search_part_name: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="공급업체 ID" width={140}>
|
||||
<Input value={search.search_partner} onChange={(e) => setSearch({ ...search, search_partner: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="입고결과" width={120}>
|
||||
<HardcodedSelect value={search.search_delivery_status} onChange={(v) => setSearch({ ...search, search_delivery_status: v })} options={DELIVERY_STATUS_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="제품구분 ID" width={120}>
|
||||
<Input value={search.search_product_cd} onChange={(e) => setSearch({ ...search, search_product_cd: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사여부" width={110}>
|
||||
<HardcodedSelect value={search.search_inspection_yn} onChange={(v) => setSearch({ ...search, search_inspection_yn: v })} options={INSPECTION_YN_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="요청현황" width={120}>
|
||||
<HardcodedSelect value={search.search_request_status} onChange={(v) => setSearch({ ...search, search_request_status: v })} options={REQUEST_STATUS_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="요청자 ID" width={120}>
|
||||
<Input value={search.request_user_id} onChange={(e) => setSearch({ ...search, request_user_id: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="요청일" width={280}>
|
||||
<CompactDateRange
|
||||
from={search.request_start_date} setFrom={(v) => setSearch({ ...search, request_start_date: v })}
|
||||
to={search.request_end_date} setTo={(v) => setSearch({ ...search, request_end_date: v })}
|
||||
/>
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
<DataGrid
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 공정검사 관리 (IPQC) — wace_plm processInspectionList.jsp 이식.
|
||||
*
|
||||
* 마스터별 검사 N건을 SUM 으로 집계해 1행 표시 (검사수량/불량수량 합계).
|
||||
* 공정검사 관리 — wace_plm 의 processInspectionList.jsp 1:1 이식.
|
||||
* 필터 10종 (2행), 그리드 9컬럼 (검사결과 OK/NG 자동산정).
|
||||
*/
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
||||
@@ -17,21 +17,21 @@ import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/compon
|
||||
import { qualityApi, ProcessInspectionRow } from "@/lib/api/quality";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// wace_plm processInspectionList.jsp 그리드 1:1
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "inspection_date", label: "검사일", width: "w-[120px]", align: "center", frozen: true },
|
||||
{ key: "inspector_name", label: "검사자", width: "w-[110px]", align: "center" },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[150px]" },
|
||||
{ key: "inspection_date", label: "검사일", width: "w-[110px]", align: "center", frozen: true },
|
||||
{ key: "inspector_name", label: "검사자", width: "w-[100px]", align: "center" },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[150px]", align: "center" },
|
||||
{ key: "product_name", label: "제품구분", width: "w-[110px]", align: "center" },
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]" },
|
||||
{ key: "part_no", label: "품번", width: "w-[180px]" },
|
||||
{ key: "part_name", label: "품명", width: "w-[200px]" },
|
||||
{ key: "inspection_qty", label: "검사수량 합계", width: "w-[140px]", align: "right", formatNumber: true },
|
||||
{ key: "defect_qty", label: "불량수량 합계", width: "w-[140px]", align: "right", formatNumber: true },
|
||||
{ key: "work_env_status", label: "작업환경상태", width: "w-[120px]", align: "center" },
|
||||
{ key: "measuring_device", label: "측정기", width: "w-[100px]", align: "center" },
|
||||
{ key: "inspection_qty", label: "검사수량 합계", width: "w-[120px]", align: "right", formatNumber: true },
|
||||
{ key: "inspection_result", label: "검사결과", width: "w-[100px]", align: "center" },
|
||||
{ key: "file_count", label: "첨부파일", width: "w-[100px]", align: "center", renderType: "clip" },
|
||||
{ key: "process_inspection_file_cnt", label: "첨부파일", width: "w-[100px]", align: "center", renderType: "clip" },
|
||||
];
|
||||
|
||||
const OK_NG_OPTIONS = ["전체", "OK", "NG"];
|
||||
|
||||
export default function ProcessInspectionPage() {
|
||||
const { user } = useAuth();
|
||||
const [rows, setRows] = useState<ProcessInspectionRow[]>([]);
|
||||
@@ -39,34 +39,48 @@ export default function ProcessInspectionPage() {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const [search, setSearch] = useState({
|
||||
project_no: "",
|
||||
search_project_no: "",
|
||||
productType: "",
|
||||
part_name: "",
|
||||
inspector_id: "",
|
||||
from: "",
|
||||
to: "",
|
||||
search_part_no: "",
|
||||
search_part_name: "",
|
||||
search_work_env_status: "",
|
||||
search_measuring_device: "",
|
||||
search_inspection_date_from: "",
|
||||
search_inspection_date_to: "",
|
||||
search_inspector: "",
|
||||
search_inspection_result: "",
|
||||
search_process_cd: "",
|
||||
});
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
if (!user) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = {};
|
||||
Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; });
|
||||
const res = await qualityApi.processInspection(params);
|
||||
const res = await qualityApi.processInspection(search);
|
||||
setRows(res.list.map((r) => ({ ...r, id: r.objid } as any)));
|
||||
setSelectedId(null);
|
||||
} catch {
|
||||
toast.error("공정검사 목록 조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch { toast.error("공정검사 목록 조회 실패"); }
|
||||
finally { setLoading(false); }
|
||||
}, [user, search]);
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
const handleReset = () =>
|
||||
setSearch({ project_no: "", productType: "", part_name: "", inspector_id: "", from: "", to: "" });
|
||||
const handleReset = () => setSearch({
|
||||
search_project_no: "", productType: "",
|
||||
search_part_no: "", search_part_name: "",
|
||||
search_work_env_status: "", search_measuring_device: "",
|
||||
search_inspection_date_from: "", search_inspection_date_to: "",
|
||||
search_inspector: "", search_inspection_result: "", search_process_cd: "",
|
||||
});
|
||||
|
||||
const HardcodedSelect = ({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: string[] }) => (
|
||||
<Select value={value || "all"} onValueChange={(v) => onChange(v === "all" || v === "전체" ? "" : v)}>
|
||||
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="전체" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((o) => <SelectItem key={o} value={o === "전체" ? "all" : o}>{o}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
@@ -81,28 +95,39 @@ export default function ProcessInspectionPage() {
|
||||
}
|
||||
/>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="프로젝트번호" width={160}>
|
||||
<Input value={search.project_no}
|
||||
onChange={(e) => setSearch({ ...search, project_no: e.target.value })} />
|
||||
<CompactFilterField label="프로젝트번호" width={150}>
|
||||
<Input value={search.search_project_no} onChange={(e) => setSearch({ ...search, search_project_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="제품구분" width={140}>
|
||||
<Input value={search.productType}
|
||||
onChange={(e) => setSearch({ ...search, productType: e.target.value })} />
|
||||
<CompactFilterField label="제품구분 ID" width={130}>
|
||||
<Input value={search.productType} onChange={(e) => setSearch({ ...search, productType: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="품번" width={140}>
|
||||
<Input value={search.search_part_no} onChange={(e) => setSearch({ ...search, search_part_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="품명" width={160}>
|
||||
<Input value={search.part_name}
|
||||
onChange={(e) => setSearch({ ...search, part_name: e.target.value })} />
|
||||
<Input value={search.search_part_name} onChange={(e) => setSearch({ ...search, search_part_name: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사자 ID" width={130}>
|
||||
<Input value={search.inspector_id}
|
||||
onChange={(e) => setSearch({ ...search, inspector_id: e.target.value })} />
|
||||
<CompactFilterField label="작업환경상태" width={120}>
|
||||
<HardcodedSelect value={search.search_work_env_status} onChange={(v) => setSearch({ ...search, search_work_env_status: v })} options={OK_NG_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="측정기" width={120}>
|
||||
<HardcodedSelect value={search.search_measuring_device} onChange={(v) => setSearch({ ...search, search_measuring_device: v })} options={OK_NG_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사일" width={280}>
|
||||
<CompactDateRange
|
||||
from={search.from} setFrom={(v) => setSearch({ ...search, from: v })}
|
||||
to={search.to} setTo={(v) => setSearch({ ...search, to: v })}
|
||||
from={search.search_inspection_date_from} setFrom={(v) => setSearch({ ...search, search_inspection_date_from: v })}
|
||||
to={search.search_inspection_date_to} setTo={(v) => setSearch({ ...search, search_inspection_date_to: v })}
|
||||
/>
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사자 ID" width={130}>
|
||||
<Input value={search.search_inspector} onChange={(e) => setSearch({ ...search, search_inspector: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사결과" width={110}>
|
||||
<HardcodedSelect value={search.search_inspection_result} onChange={(v) => setSearch({ ...search, search_inspection_result: v })} options={OK_NG_OPTIONS} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="진행공정 ID" width={130}>
|
||||
<Input value={search.search_process_cd} onChange={(e) => setSearch({ ...search, search_process_cd: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
<DataGrid
|
||||
gridId="quality-process-inspection"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 반제품검사 관리 — wace_plm semiProductInspectionList.jsp 이식.
|
||||
*
|
||||
* 입고/양품/불량/재생/최종양품 수량과 불량률을 1행에 집계한다.
|
||||
* 반제품검사 관리 — wace_plm 의 semiProductInspectionList.jsp 1:1 이식.
|
||||
* 필터 8종 (2행), 그리드 14컬럼 (재생수량/최종양품 자동산정).
|
||||
* 마스터 행은 data_type='GOOD' 이고, 동일 inspection_group_id 의 'DEFECT' 행을 SUM.
|
||||
*/
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
@@ -17,20 +17,21 @@ import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/compon
|
||||
import { qualityApi, SemiProductInspectionRow } from "@/lib/api/quality";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// wace_plm semiProductInspectionList.jsp 그리드 1:1
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "inspection_date", label: "검사일", width: "w-[120px]", align: "center", frozen: true },
|
||||
{ key: "writer_name", label: "검사자", width: "w-[110px]", align: "center" },
|
||||
{ key: "model_name", label: "품명(모델명)", width: "w-[180px]" },
|
||||
{ key: "product_type", label: "제품구분", width: "w-[110px]", align: "center" },
|
||||
{ key: "work_order_no", label: "작업지시번호", width: "w-[150px]" },
|
||||
{ key: "part_no", label: "부품품번", width: "w-[140px]" },
|
||||
{ key: "part_name", label: "부품명", width: "w-[180px]" },
|
||||
{ key: "receipt_qty", label: "입고수량", width: "w-[110px]", align: "right", formatNumber: true },
|
||||
{ key: "good_qty", label: "양품수량", width: "w-[110px]", align: "right", formatNumber: true },
|
||||
{ key: "defective_qty", label: "불량수량", width: "w-[110px]", align: "right", formatNumber: true },
|
||||
{ key: "defect_rate", label: "불량률(%)", width: "w-[100px]", align: "right" },
|
||||
{ key: "regeneration_qty",label: "재생수량", width: "w-[110px]", align: "right", formatNumber: true },
|
||||
{ key: "final_good_qty", label: "최종양품수량", width: "w-[130px]", align: "right", formatNumber: true },
|
||||
{ key: "inspection_date", label: "검사일", width: "w-[140px]", align: "center", frozen: true },
|
||||
{ key: "writer_name", label: "검사자", width: "w-[110px]", align: "center" },
|
||||
{ key: "product_type", label: "제품구분", width: "w-[110px]", align: "center" },
|
||||
{ key: "model_name", label: "품명(모델명)", width: "w-[150px]" },
|
||||
{ key: "work_order_no", label: "작업지시번호", width: "w-[140px]", align: "center" },
|
||||
{ key: "part_no", label: "부품품번", width: "w-[150px]" },
|
||||
{ key: "part_name", label: "부품명", width: "w-[180px]" },
|
||||
{ key: "receipt_qty", label: "입고수량 합계", width: "w-[120px]", align: "right", formatNumber: true },
|
||||
{ key: "good_qty", label: "양품수량 합계", width: "w-[120px]", align: "right", formatNumber: true },
|
||||
{ key: "defective_qty", label: "불량수량 합계", width: "w-[120px]", align: "right", formatNumber: true },
|
||||
{ key: "defect_rate", label: "불량률(%)", width: "w-[100px]", align: "right" },
|
||||
{ key: "regeneration_qty",label: "재생수량 합계", width: "w-[120px]", align: "right", formatNumber: true },
|
||||
{ key: "final_good_qty", label: "최종양품수량 합계", width: "w-[140px]", align: "right", formatNumber: true },
|
||||
];
|
||||
|
||||
export default function SemiProductInspectionPage() {
|
||||
@@ -40,34 +41,47 @@ export default function SemiProductInspectionPage() {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const [search, setSearch] = useState({
|
||||
model_name: "",
|
||||
part_no: "",
|
||||
part_name: "",
|
||||
writer: "",
|
||||
from: "",
|
||||
to: "",
|
||||
search_model_name: "",
|
||||
search_work_order_no: "",
|
||||
search_part_no: "",
|
||||
search_part_name: "",
|
||||
inspection_start_date: "",
|
||||
inspection_end_date: "",
|
||||
search_writer: "",
|
||||
search_defect_type: "",
|
||||
search_responsible_dept: "",
|
||||
});
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
if (!user) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = {};
|
||||
Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; });
|
||||
const res = await qualityApi.semiProductInspection(params);
|
||||
const res = await qualityApi.semiProductInspection(search);
|
||||
setRows(res.list.map((r) => ({ ...r, id: r.objid } as any)));
|
||||
setSelectedId(null);
|
||||
} catch {
|
||||
toast.error("반제품검사 목록 조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch { toast.error("반제품검사 목록 조회 실패"); }
|
||||
finally { setLoading(false); }
|
||||
}, [user, search]);
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
const handleReset = () =>
|
||||
setSearch({ model_name: "", part_no: "", part_name: "", writer: "", from: "", to: "" });
|
||||
const handleReset = () => setSearch({
|
||||
search_model_name: "", search_work_order_no: "",
|
||||
search_part_no: "", search_part_name: "",
|
||||
inspection_start_date: "", inspection_end_date: "",
|
||||
search_writer: "", search_defect_type: "", search_responsible_dept: "",
|
||||
});
|
||||
|
||||
const summary = (() => {
|
||||
const fmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "총 입고수량", value: fmt(rows.reduce((a, r) => a + Number(r.receipt_qty || 0), 0)) },
|
||||
{ label: "총 양품수량", value: fmt(rows.reduce((a, r) => a + Number(r.good_qty || 0), 0)) },
|
||||
{ label: "총 불량수량", value: fmt(rows.reduce((a, r) => a + Number(r.defective_qty || 0), 0)) },
|
||||
{ label: "총 재생수량", value: fmt(rows.reduce((a, r) => a + Number(r.regeneration_qty || 0), 0)) },
|
||||
{ label: "총 최종양품수량", value: fmt(rows.reduce((a, r) => a + Number(r.final_good_qty || 0), 0)) },
|
||||
];
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
@@ -82,28 +96,33 @@ export default function SemiProductInspectionPage() {
|
||||
}
|
||||
/>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="모델명" width={160}>
|
||||
<Input value={search.model_name}
|
||||
onChange={(e) => setSearch({ ...search, model_name: e.target.value })} />
|
||||
<CompactFilterField label="품명(모델명)" width={160}>
|
||||
<Input value={search.search_model_name} onChange={(e) => setSearch({ ...search, search_model_name: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="부품품번" width={140}>
|
||||
<Input value={search.part_no}
|
||||
onChange={(e) => setSearch({ ...search, part_no: e.target.value })} />
|
||||
<CompactFilterField label="작업지시번호" width={150}>
|
||||
<Input value={search.search_work_order_no} onChange={(e) => setSearch({ ...search, search_work_order_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="부품품번" width={150}>
|
||||
<Input value={search.search_part_no} onChange={(e) => setSearch({ ...search, search_part_no: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="부품명" width={160}>
|
||||
<Input value={search.part_name}
|
||||
onChange={(e) => setSearch({ ...search, part_name: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사자 ID" width={130}>
|
||||
<Input value={search.writer}
|
||||
onChange={(e) => setSearch({ ...search, writer: e.target.value })} />
|
||||
<Input value={search.search_part_name} onChange={(e) => setSearch({ ...search, search_part_name: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사일" width={280}>
|
||||
<CompactDateRange
|
||||
from={search.from} setFrom={(v) => setSearch({ ...search, from: v })}
|
||||
to={search.to} setTo={(v) => setSearch({ ...search, to: v })}
|
||||
from={search.inspection_start_date} setFrom={(v) => setSearch({ ...search, inspection_start_date: v })}
|
||||
to={search.inspection_end_date} setTo={(v) => setSearch({ ...search, inspection_end_date: v })}
|
||||
/>
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="검사자 ID" width={120}>
|
||||
<Input value={search.search_writer} onChange={(e) => setSearch({ ...search, search_writer: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="불량유형 ID" width={130}>
|
||||
<Input value={search.search_defect_type} onChange={(e) => setSearch({ ...search, search_defect_type: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
<CompactFilterField label="귀책부서 ID" width={130}>
|
||||
<Input value={search.search_responsible_dept} onChange={(e) => setSearch({ ...search, search_responsible_dept: e.target.value })} />
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
<DataGrid
|
||||
gridId="quality-semi-product-inspection"
|
||||
@@ -116,6 +135,7 @@ export default function SemiProductInspectionPage() {
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
emptyMessage="조회된 반제품검사 내역이 없습니다."
|
||||
summaryStats={summary}
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
|
||||
+37
-18
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* 품질관리 API 클라이언트 — 4개 메뉴 list 엔드포인트.
|
||||
* 품질관리 API 클라이언트 — wace_plm quality.xml 1:1.
|
||||
* 백엔드: backend-node/src/routes/qualityRoutes.ts
|
||||
*/
|
||||
import { apiClient } from "./client";
|
||||
|
||||
export interface IncomingRequestRow {
|
||||
objid: string;
|
||||
purchase_order_no: string;
|
||||
proposal_no?: string;
|
||||
purchase_order_no?: string;
|
||||
project_no?: string;
|
||||
product_name?: string;
|
||||
part_no?: string;
|
||||
@@ -20,13 +20,26 @@ export interface IncomingRequestRow {
|
||||
request_status?: string;
|
||||
}
|
||||
|
||||
export interface IncomingMgmtRow extends IncomingRequestRow {
|
||||
inspector_name?: string;
|
||||
export interface IncomingMgmtRow {
|
||||
objid: string;
|
||||
inspection_date?: string;
|
||||
total_qty?: number;
|
||||
good_qty?: number;
|
||||
bad_qty?: number;
|
||||
inspector_name?: string;
|
||||
proposal_no?: string;
|
||||
purchase_order_no?: string;
|
||||
project_no?: string;
|
||||
product_name?: string;
|
||||
model_name?: string;
|
||||
part_no?: string;
|
||||
part_name?: string;
|
||||
partner_name?: string;
|
||||
delivery_date?: string;
|
||||
delivery_qty?: number;
|
||||
delivery_status?: string;
|
||||
inspection_qty?: number;
|
||||
defect_qty_sum?: number;
|
||||
defect_rate?: number;
|
||||
inspection_result?: string;
|
||||
inspection_file_cnt?: number;
|
||||
}
|
||||
|
||||
export interface ProcessInspectionRow {
|
||||
@@ -42,15 +55,15 @@ export interface ProcessInspectionRow {
|
||||
work_env_status?: string;
|
||||
measuring_device?: string;
|
||||
inspection_result?: string;
|
||||
file_count?: number;
|
||||
process_inspection_file_cnt?: number;
|
||||
}
|
||||
|
||||
export interface SemiProductInspectionRow {
|
||||
objid: string;
|
||||
inspection_date?: string;
|
||||
writer_name?: string;
|
||||
model_name?: string;
|
||||
product_type?: string;
|
||||
model_name?: string;
|
||||
work_order_no?: string;
|
||||
part_no?: string;
|
||||
part_name?: string;
|
||||
@@ -68,13 +81,19 @@ interface ListResp<T> {
|
||||
pagination: { page: number; pageSize: number; total: number; totalPages: number };
|
||||
}
|
||||
|
||||
export const qualityApi = {
|
||||
incomingRequest: async (params: Record<string, string> = {}): Promise<ListResp<IncomingRequestRow>> =>
|
||||
(await apiClient.get("/quality/incoming-request", { params })).data,
|
||||
incomingMgmt: async (params: Record<string, string> = {}): Promise<ListResp<IncomingMgmtRow>> =>
|
||||
(await apiClient.get("/quality/incoming-mgmt", { params })).data,
|
||||
processInspection: async (params: Record<string, string> = {}): Promise<ListResp<ProcessInspectionRow>> =>
|
||||
(await apiClient.get("/quality/process-inspection", { params })).data,
|
||||
semiProductInspection: async (params: Record<string, string> = {}): Promise<ListResp<SemiProductInspectionRow>> =>
|
||||
(await apiClient.get("/quality/semi-product-inspection", { params })).data,
|
||||
const buildParams = (obj: Record<string, string | undefined>): Record<string, string> => {
|
||||
const p: Record<string, string> = {};
|
||||
Object.entries(obj).forEach(([k, v]) => { if (v) p[k] = v; });
|
||||
return p;
|
||||
};
|
||||
|
||||
export const qualityApi = {
|
||||
incomingRequest: async (params: Record<string, string | undefined> = {}): Promise<ListResp<IncomingRequestRow>> =>
|
||||
(await apiClient.get("/quality/incoming-request", { params: buildParams(params) })).data,
|
||||
incomingMgmt: async (params: Record<string, string | undefined> = {}): Promise<ListResp<IncomingMgmtRow>> =>
|
||||
(await apiClient.get("/quality/incoming-mgmt", { params: buildParams(params) })).data,
|
||||
processInspection: async (params: Record<string, string | undefined> = {}): Promise<ListResp<ProcessInspectionRow>> =>
|
||||
(await apiClient.get("/quality/process-inspection", { params: buildParams(params) })).data,
|
||||
semiProductInspection: async (params: Record<string, string | undefined> = {}): Promise<ListResp<SemiProductInspectionRow>> =>
|
||||
(await apiClient.get("/quality/semi-product-inspection", { params: buildParams(params) })).data,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user