Update date handling in inventory and sales order pages
- Refactored date handling in the InventoryStatusPage to use `toLocaleString` for transaction dates and last in dates, ensuring correct timezone formatting. - Introduced FormDatePicker in SalesOrderPage for date inputs, enhancing user experience with automatic formatting and improved date handling. - Added a checkbox for filtering items by customer in SalesOrderPage, allowing users to view only items registered for the selected customer. This update improves date accuracy and user interaction in the inventory and sales order modules.
This commit is contained in:
116
backend-node/src/controllers/qualityMonitoringController.ts
Normal file
116
backend-node/src/controllers/qualityMonitoringController.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 품질 모니터링 데이터 조회 (서버 페이징 + 통계 합산)
|
||||
* work_order_process(공정 메타) + work_order_process_result(실적) JOIN
|
||||
* - 페이지: 화면 표 표시용
|
||||
* - summary: KPI 카드용 (전체 합산)
|
||||
*/
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export async function getQualityMonitoringData(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const from = (req.query.from as string | undefined)?.trim() || "";
|
||||
const to = (req.query.to as string | undefined)?.trim() || "";
|
||||
const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10) || 1);
|
||||
const size = Math.max(1, Math.min(500, parseInt(String(req.query.size ?? "50"), 10) || 50));
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
const params: any[] = [companyCode];
|
||||
const conds: string[] = ["wopr.company_code = $1"];
|
||||
if (from) {
|
||||
params.push(`${from} 00:00:00`);
|
||||
conds.push(`wopr.created_date >= $${params.length}::timestamp`);
|
||||
}
|
||||
if (to) {
|
||||
params.push(`${to} 23:59:59`);
|
||||
conds.push(`wopr.created_date <= $${params.length}::timestamp`);
|
||||
}
|
||||
const whereClause = `WHERE ${conds.join(" AND ")}`;
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// 1) total + summary (KPI 카드)
|
||||
const summaryQuery = `
|
||||
SELECT
|
||||
COUNT(*)::int AS total,
|
||||
SUM(CASE WHEN wopr.status = 'completed' AND COALESCE(CAST(NULLIF(wopr.defect_qty, '') AS numeric), 0) = 0 THEN 1 ELSE 0 END)::int AS passed,
|
||||
SUM(CASE WHEN wopr.status = 'completed' AND COALESCE(CAST(NULLIF(wopr.defect_qty, '') AS numeric), 0) > 0 THEN 1 ELSE 0 END)::int AS failed,
|
||||
SUM(CASE WHEN wopr.status <> 'completed' OR wopr.status IS NULL THEN 1 ELSE 0 END)::int AS pending
|
||||
FROM work_order_process_result wopr
|
||||
${whereClause}
|
||||
`;
|
||||
const summaryRes = await pool.query(summaryQuery, params);
|
||||
const summaryRow = summaryRes.rows[0] || { total: 0, passed: 0, failed: 0, pending: 0 };
|
||||
const total = summaryRow.total || 0;
|
||||
const passRate = total > 0 ? Math.round((summaryRow.passed / total) * 1000) / 10 : 0;
|
||||
|
||||
// 2) 페이지 데이터
|
||||
const pageParams = [...params, size, offset];
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
wopr.id,
|
||||
wopr.wop_id,
|
||||
wopr.status,
|
||||
wopr.input_qty,
|
||||
wopr.good_qty,
|
||||
wopr.defect_qty,
|
||||
wopr.started_at,
|
||||
wopr.completed_at,
|
||||
wopr.completed_by,
|
||||
wopr.accepted_by,
|
||||
wop.wo_id,
|
||||
wop.process_code,
|
||||
wop.process_name,
|
||||
wop.plan_qty
|
||||
FROM work_order_process_result wopr
|
||||
LEFT JOIN work_order_process wop
|
||||
ON wop.id = wopr.wop_id AND wop.company_code = wopr.company_code
|
||||
${whereClause}
|
||||
ORDER BY wopr.created_date DESC
|
||||
LIMIT $${params.length + 1} OFFSET $${params.length + 2}
|
||||
`;
|
||||
const dataRes = await pool.query(dataQuery, pageParams);
|
||||
|
||||
const rows = dataRes.rows.map((r: any) => ({
|
||||
id: r.id,
|
||||
wo_id: r.wo_id,
|
||||
process_code: r.process_code || "",
|
||||
process_name: r.process_name || "",
|
||||
status: r.status || "",
|
||||
plan_qty: Number(r.plan_qty) || 0,
|
||||
input_qty: Number(r.input_qty) || 0,
|
||||
good_qty: Number(r.good_qty) || 0,
|
||||
defect_qty: Number(r.defect_qty) || 0,
|
||||
started_at: r.started_at || null,
|
||||
completed_at: r.completed_at || null,
|
||||
worker_name: r.completed_by || r.accepted_by || "",
|
||||
}));
|
||||
|
||||
logger.info("품질 모니터링 조회", {
|
||||
companyCode, from, to, page, size, total,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
rows,
|
||||
total,
|
||||
page,
|
||||
size,
|
||||
totalPages: Math.max(1, Math.ceil(total / size)),
|
||||
summary: {
|
||||
total,
|
||||
passed: summaryRow.passed || 0,
|
||||
failed: summaryRow.failed || 0,
|
||||
pending: summaryRow.pending || 0,
|
||||
passRate,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("품질 모니터링 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
@@ -220,16 +220,9 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
const insertedDetails: any[] = [];
|
||||
|
||||
// 기존 디테일이 있으면 스킵 (멱등성 — 같은 inbound_number로 2번 호출 방지)
|
||||
const existingDetails = await client.query(
|
||||
`SELECT COUNT(*) AS cnt FROM inbound_detail WHERE company_code = $1 AND inbound_id = $2`,
|
||||
[companyCode, inboundNumber]
|
||||
);
|
||||
if (parseInt(existingDetails.rows[0].cnt, 10) > 0) {
|
||||
await client.query("COMMIT");
|
||||
client.release();
|
||||
return res.json({ success: true, data: [], message: "이미 등록된 입고입니다." });
|
||||
}
|
||||
// 멱등성 체크는 제거 — 수정 모달에서 "기존 입고에 새 품목 추가" 케이스가 차단되던 버그.
|
||||
// 더블클릭 방지는 프론트 setSaving 가드로 처리. 같은 inbound_number에 detail이 이미 있어도
|
||||
// 새 품목 추가는 정상 흐름이므로 그대로 INSERT 진행.
|
||||
|
||||
// 2. 디테일 INSERT (inbound_detail) + 재고/발주 업데이트
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
@@ -1312,6 +1305,8 @@ export async function getProductionResults(
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// 실적 컬럼(good_qty/concession_qty/result_status/is_rework)은 work_order_process가 아닌
|
||||
// work_order_process_result에 존재 → wopr_agg로 LEFT JOIN하여 합계/대표값 사용
|
||||
const dataResult = await pool.query(
|
||||
`SELECT
|
||||
wop.id,
|
||||
@@ -1325,14 +1320,12 @@ export async function getProductionResults(
|
||||
COALESCE(ii.item_name, ii.item_number, wi.item_id) AS item_name,
|
||||
COALESCE(ii.size, '') AS spec,
|
||||
COALESCE(ii.material, '') AS material,
|
||||
COALESCE(CAST(NULLIF(wop.good_qty, '') AS numeric), 0)
|
||||
+ COALESCE(CAST(NULLIF(wop.concession_qty, '') AS numeric), 0) AS order_qty,
|
||||
COALESCE(wopr_agg.sum_good, 0) + COALESCE(wopr_agg.sum_concession, 0) AS order_qty,
|
||||
COALESCE(rcv.received_qty, 0) AS received_qty,
|
||||
COALESCE(CAST(NULLIF(wop.good_qty, '') AS numeric), 0)
|
||||
+ COALESCE(CAST(NULLIF(wop.concession_qty, '') AS numeric), 0)
|
||||
COALESCE(wopr_agg.sum_good, 0) + COALESCE(wopr_agg.sum_concession, 0)
|
||||
- COALESCE(rcv.received_qty, 0) AS remain_qty,
|
||||
'work_order_process' AS source_table,
|
||||
wop.result_status,
|
||||
wopr_agg.last_status AS result_status,
|
||||
COALESCE(ii.image, NULL) AS image,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM item_inspection_info iii
|
||||
@@ -1348,6 +1341,16 @@ export async function getProductionResults(
|
||||
FROM item_info
|
||||
ORDER BY id, company_code, created_date DESC
|
||||
) ii ON wi.item_id = ii.id AND wi.company_code = ii.company_code
|
||||
LEFT JOIN (
|
||||
SELECT wop_id, company_code,
|
||||
SUM(COALESCE(CAST(NULLIF(good_qty, '') AS numeric), 0)) AS sum_good,
|
||||
SUM(COALESCE(CAST(NULLIF(concession_qty, '') AS numeric), 0)) AS sum_concession,
|
||||
SUM(CASE WHEN is_rework = 'Y' THEN 1 ELSE 0 END) AS rework_count,
|
||||
MAX(result_status) AS last_status
|
||||
FROM work_order_process_result
|
||||
WHERE company_code = $1
|
||||
GROUP BY wop_id, company_code
|
||||
) wopr_agg ON wopr_agg.wop_id = wop.id AND wopr_agg.company_code = wop.company_code
|
||||
LEFT JOIN (
|
||||
SELECT im.source_id,
|
||||
SUM(COALESCE(CAST(NULLIF(id.inbound_qty::text, '') AS numeric), 0)) AS received_qty
|
||||
@@ -1362,11 +1365,10 @@ export async function getProductionResults(
|
||||
WHERE wop.company_code = $1
|
||||
AND wop.process_code = $2
|
||||
AND wop.parent_process_id IS NULL
|
||||
AND (wop.is_rework IS NULL OR wop.is_rework != 'Y')
|
||||
AND COALESCE(CAST(NULLIF(wop.good_qty, '') AS numeric), 0) > 0
|
||||
AND COALESCE(wopr_agg.rework_count, 0) = 0
|
||||
AND COALESCE(wopr_agg.sum_good, 0) > 0
|
||||
AND (
|
||||
COALESCE(CAST(NULLIF(wop.good_qty, '') AS numeric), 0)
|
||||
+ COALESCE(CAST(NULLIF(wop.concession_qty, '') AS numeric), 0)
|
||||
COALESCE(wopr_agg.sum_good, 0) + COALESCE(wopr_agg.sum_concession, 0)
|
||||
- COALESCE(rcv.received_qty, 0)
|
||||
) > 0
|
||||
${keywordCondition}
|
||||
|
||||
@@ -220,18 +220,23 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
COALESCE(i.item_name, d.part_name, m.part_name, COALESCE(d.part_code, m.part_code, '')) AS part_name,
|
||||
COALESCE(d.spec, m.spec, '') AS spec,
|
||||
COALESCE(m.material, '') AS material,
|
||||
COALESCE(c.customer_name, '') AS customer_name,
|
||||
COALESCE(NULLIF(c.customer_name, ''), m.partner_id, d.delivery_partner_code, '') AS customer_name,
|
||||
COALESCE(m.partner_id, d.delivery_partner_code, '') AS partner_code,
|
||||
COALESCE(d.due_date, m.due_date::text, '') AS due_date,
|
||||
COALESCE(NULLIF(d.due_date, ''), NULLIF(m.due_date::text, ''), NULLIF(m.item_due_date::text, ''), '') AS due_date,
|
||||
COALESCE(NULLIF(d.qty,'')::numeric, m.order_qty, 0) AS order_qty,
|
||||
COALESCE(NULLIF(d.ship_qty,'')::numeric, m.ship_qty, 0) AS shipped_qty
|
||||
COALESCE(NULLIF(d.ship_qty,'')::numeric, m.ship_qty, 0) AS shipped_qty,
|
||||
COALESCE(NULLIF(d.width, ''), i.width::text, '') AS width,
|
||||
COALESCE(NULLIF(d.height, ''), i.height::text, '') AS height,
|
||||
COALESCE(NULLIF(d.thickness, ''), i.thickness::text, '') AS thickness
|
||||
FROM shipment_plan sp
|
||||
LEFT JOIN sales_order_detail d
|
||||
ON sp.detail_id = d.id AND sp.company_code = d.company_code
|
||||
LEFT JOIN sales_order_mng m
|
||||
ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||||
LEFT JOIN sales_order_detail d
|
||||
ON sp.company_code = d.company_code
|
||||
AND (d.id = sp.detail_id::text
|
||||
OR (d.order_no = m.order_no AND d.part_code = m.part_code))
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
SELECT item_name, width, height, thickness FROM item_info
|
||||
WHERE item_number = COALESCE(d.part_code, m.part_code)
|
||||
AND company_code = sp.company_code
|
||||
LIMIT 1
|
||||
@@ -250,10 +255,12 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
const countQuery = `
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM shipment_plan sp
|
||||
LEFT JOIN sales_order_detail d
|
||||
ON sp.detail_id = d.id AND sp.company_code = d.company_code
|
||||
LEFT JOIN sales_order_mng m
|
||||
ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||||
LEFT JOIN sales_order_detail d
|
||||
ON sp.company_code = d.company_code
|
||||
AND (d.id = sp.detail_id::text
|
||||
OR (d.order_no = m.order_no AND d.part_code = m.part_code))
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
WHERE item_number = COALESCE(d.part_code, m.part_code)
|
||||
|
||||
Reference in New Issue
Block a user