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:
kjs
2026-04-29 18:20:01 +09:00
parent ef11b4d83b
commit 6ddc84f285
35 changed files with 813 additions and 370 deletions

View 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 });
}
}

View File

@@ -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}

View File

@@ -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)