Refactor analytics report and production plan services for improved data handling

- Updated the `getQualityReportData` function to utilize `inspection_result_mng` for quality report generation, enhancing data accuracy by aggregating inspection results.
- Refined date handling in the `getOrderSummary` function to improve filtering logic and ensure accurate stock calculations.
- Implemented virtual scrolling in the `TimelineScheduler` component to optimize performance when rendering large datasets.

These changes enhance data retrieval efficiency and user experience across analytics and production planning modules.
This commit is contained in:
kjs
2026-04-29 21:05:38 +09:00
parent 5f9c876f9e
commit d549947fac
6 changed files with 150 additions and 93 deletions

View File

@@ -367,57 +367,33 @@ export async function getQualityReportData(req: any, res: Response): Promise<voi
const params: any[] = [];
let idx = 1;
const cf = buildCompanyFilter(companyCode, "pr", idx);
const cf = buildCompanyFilter(companyCode, "irm", idx);
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
const df = buildDateFilter(startDate, endDate, "COALESCE(pr.production_date, pr.created_date::date::text)", idx);
const df = buildDateFilter(startDate, endDate, "TO_CHAR(irm.inspection_date, 'YYYY-MM-DD')", idx);
conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx;
const whereClause = buildWhereClause(conditions);
// 품질 리포트는 실제 검사 결과(inspection_result_mng) 기반으로 집계.
// 작업/생산실적이 아닌 검사 단위에서 합격/불량 수량을 산출.
const dataQuery = `
SELECT
COALESCE(pr.production_date, pr.created_date::date::text) as date,
COALESCE(NULLIF(ii.item_name, ''), NULLIF(ii.item_number, ''), '미지정') as item,
'일반검사' as "defectType",
COALESCE(wi.routing, '미지정') as process,
COALESCE(pr.worker_name, '미지정') as inspector,
CAST(COALESCE(NULLIF(pr.production_qty, ''), '0') AS numeric) as "inspQty",
CAST(COALESCE(NULLIF(pr.production_qty, ''), '0') AS numeric)
- CAST(COALESCE(NULLIF(pr.defect_qty, ''), '0') AS numeric) as "passQty",
CAST(COALESCE(NULLIF(pr.defect_qty, ''), '0') AS numeric) as "defectQty",
0 as "reworkQty",
0 as "scrapQty",
0 as "claimCnt",
pr.company_code
FROM production_record pr
LEFT JOIN work_instruction wi ON pr.wo_id = wi.id AND pr.company_code = wi.company_code
LEFT JOIN LATERAL (
SELECT ii_inner.item_number, ii_inner.item_name
FROM item_info ii_inner
WHERE ii_inner.company_code = wi.company_code
AND (
(NULLIF(wi.item_id, '') IS NOT NULL
AND (ii_inner.id = wi.item_id OR ii_inner.item_number = wi.item_id))
OR ii_inner.item_number = (
SELECT wid.item_number
FROM work_instruction_detail wid
WHERE wid.work_instruction_id = wi.id
AND wid.company_code = wi.company_code
AND NULLIF(wid.item_number, '') IS NOT NULL
ORDER BY wid.created_date ASC
LIMIT 1
)
)
ORDER BY
CASE WHEN ii_inner.id = wi.item_id THEN 1
WHEN ii_inner.item_number = wi.item_id THEN 2
ELSE 3 END,
ii_inner.created_date DESC
LIMIT 1
) ii ON true
TO_CHAR(irm.inspection_date, 'YYYY-MM-DD') AS date,
COALESCE(NULLIF(REGEXP_REPLACE(COALESCE(irm.item_name, ''), '^[[:space:]]+|[[:space:]]+$', '', 'g'), ''), NULLIF(irm.item_code, ''), '미지정') AS item,
COALESCE(NULLIF(irm.inspection_type, ''), '일반검사') AS "defectType",
COALESCE(NULLIF(irm.inspection_type, ''), '미지정') AS process,
COALESCE(NULLIF(irm.inspector, ''), '미지정') AS inspector,
COALESCE(irm.total_qty, 0) AS "inspQty",
COALESCE(irm.good_qty, 0) AS "passQty",
COALESCE(irm.bad_qty, 0) AS "defectQty",
0 AS "reworkQty",
0 AS "scrapQty",
0 AS "claimCnt",
irm.company_code
FROM inspection_result_mng irm
${whereClause}
ORDER BY date DESC NULLS LAST
ORDER BY irm.inspection_date DESC NULLS LAST
`;
const dataRows = await query(dataQuery, params);
@@ -430,11 +406,7 @@ export async function getQualityReportData(req: any, res: Response): Promise<voi
rows: dataRows,
filterOptions: {
items: extractFilterSet(dataRows, "item"),
defectTypes: [
{ value: "외관불량", label: "외관불량" }, { value: "치수불량", label: "치수불량" },
{ value: "기능불량", label: "기능불량" }, { value: "재질불량", label: "재질불량" },
{ value: "일반검사", label: "일반검사" },
],
defectTypes: extractFilterSet(dataRows, "defectType"),
processes: extractFilterSet(dataRows, "process"),
inspectors: extractFilterSet(dataRows, "inspector"),
},

View File

@@ -131,7 +131,13 @@ export async function getOrderSummary(
LEFT JOIN stock_info si ON os.item_code = si.item_code
LEFT JOIN plan_info pi ON os.item_code = pi.item_code
LEFT JOIN item_info_dedup ilt ON os.item_code = ilt.item_number
${options?.excludePlanned ? "WHERE COALESCE(pi.existing_plan_qty, 0) + COALESCE(pi.in_progress_qty, 0) = 0" : ""}
${options?.excludePlanned
? ""
: `WHERE GREATEST(
os.total_balance_qty + COALESCE(si.safety_stock, 0) - COALESCE(si.current_stock, 0)
- COALESCE(pi.existing_plan_qty, 0) - COALESCE(pi.in_progress_qty, 0),
0
) > 0`}
ORDER BY os.item_code
`;
@@ -141,10 +147,11 @@ export async function getOrderSummary(
let pagedParams: any[] = params;
if (usePaging) {
// total: 그룹 수 = order_summary CTE의 행
// total: excludePlanned 필터까지 적용한 그룹
// order_summary + plan_info CTE만 다시 만들어서 동일 필터로 COUNT
const countQuery = `
WITH all_orders AS (
SELECT so.part_code, so.company_code
SELECT so.part_code, so.company_code, COALESCE(so.balance_qty::numeric, 0) AS balance_qty
FROM sales_order_mng so
WHERE ${whereClause}
AND so.part_code IS NOT NULL AND so.part_code != ''
@@ -153,15 +160,43 @@ export async function getOrderSummary(
WHERE sd.order_no = so.order_no AND sd.company_code = so.company_code
)
UNION ALL
SELECT sd.part_code, sd.company_code
SELECT sd.part_code, sd.company_code, COALESCE(sd.balance_qty::numeric, sd.qty::numeric - COALESCE(sd.ship_qty::numeric, 0), 0)
FROM sales_order_detail sd
INNER JOIN sales_order_mng so ON sd.order_no = so.order_no AND sd.company_code = so.company_code
WHERE sd.company_code = $1
AND sd.part_code IS NOT NULL AND sd.part_code != ''
),
os AS (
SELECT part_code AS item_code, SUM(balance_qty) AS total_balance_qty
FROM all_orders GROUP BY part_code
),
si AS (
SELECT item_code,
SUM(COALESCE(current_qty::numeric, 0)) AS current_stock,
MAX(COALESCE(safety_qty::numeric, 0)) AS safety_stock
FROM inventory_stock WHERE company_code = $1 GROUP BY item_code
),
pi AS (
SELECT item_code,
SUM(CASE WHEN status = 'planned' THEN COALESCE(plan_qty, 0) ELSE 0 END) AS existing_plan_qty,
SUM(CASE WHEN status = 'in_progress' THEN COALESCE(plan_qty, 0) ELSE 0 END) AS in_progress_qty
FROM production_plan_mng
WHERE company_code = $1
AND COALESCE(product_type, '완제품') = '완제품'
AND status NOT IN ('completed', 'cancelled')
GROUP BY item_code
)
SELECT COUNT(*)::int AS total FROM (
SELECT DISTINCT part_code FROM all_orders
) g;
SELECT COUNT(*)::int AS total
FROM os
LEFT JOIN si ON os.item_code = si.item_code
LEFT JOIN pi ON os.item_code = pi.item_code
${options?.excludePlanned
? ""
: `WHERE GREATEST(
os.total_balance_qty + COALESCE(si.safety_stock, 0) - COALESCE(si.current_stock, 0)
- COALESCE(pi.existing_plan_qty, 0) - COALESCE(pi.in_progress_qty, 0),
0
) > 0`};
`;
const countRes = await pool.query(countQuery, params);
total = countRes.rows[0]?.total ?? 0;