import { Response } from "express"; import { query } from "../database/db"; import { logger } from "../utils/logger"; /** * 영업 리포트 컨트롤러 * - 수주 데이터를 기반으로 집계/분석용 원본 데이터를 반환 * - 프론트엔드에서 그룹핑/집계/필터링 처리 */ export async function getSalesReportData( req: any, res: Response ): Promise { try { const companyCode = req.user?.companyCode; if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; } const { startDate, endDate } = req.query; const conditions: string[] = []; const params: any[] = []; let paramIdx = 1; // 멀티테넌시: 최고관리자는 전체, 일반 회사는 자기 데이터만 if (companyCode !== "*") { conditions.push(`som.company_code = $${paramIdx}`); params.push(companyCode); paramIdx++; } // 날짜 필터 (due_date 또는 order_date 기준) if (startDate) { conditions.push( `COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) >= $${paramIdx}` ); params.push(startDate); paramIdx++; } if (endDate) { conditions.push( `COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) <= $${paramIdx}` ); params.push(endDate); paramIdx++; } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const dataQuery = ` SELECT som.order_no, COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) as date, som.order_date, som.partner_id, COALESCE(cm.customer_name, som.partner_id, '미지정') as customer, sod.part_code, COALESCE(ii.item_name, sod.part_name, sod.part_code, '미지정') as item, CAST(COALESCE(NULLIF(sod.qty, ''), '0') AS numeric) as "orderQty", CAST(COALESCE(NULLIF(sod.ship_qty, ''), '0') AS numeric) as "shipQty", CAST(COALESCE(NULLIF(sod.unit_price, ''), '0') AS numeric) as "unitPrice", CAST(COALESCE(NULLIF(sod.amount, ''), '0') AS numeric) as "orderAmt", 1 as "orderCount", som.status, som.company_code FROM sales_order_mng som JOIN sales_order_detail sod ON som.order_no = sod.order_no AND som.company_code = sod.company_code LEFT JOIN customer_mng cm ON som.partner_id = cm.customer_code AND som.company_code = cm.company_code LEFT JOIN ( SELECT DISTINCT ON (item_number, company_code) item_number, item_name, company_code FROM item_info ORDER BY item_number, company_code, created_date DESC ) ii ON sod.part_code = ii.item_number AND sod.company_code = ii.company_code ${whereClause} ORDER BY date DESC NULLS LAST `; // query()는 rows 배열을 직접 반환 const dataRows = await query(dataQuery, params); // 필터 옵션 조회 (거래처, 품목, 상태) const filterParams: any[] = []; let filterWhere = ""; if (companyCode !== "*") { filterWhere = `WHERE company_code = $1`; filterParams.push(companyCode); } const statusWhere = filterWhere ? `${filterWhere} AND status IS NOT NULL` : `WHERE status IS NOT NULL`; const [customersRows, statusRows] = await Promise.all([ query( `SELECT DISTINCT customer_code as value, customer_name as label FROM customer_mng ${filterWhere} ORDER BY customer_name`, filterParams ), query( `SELECT DISTINCT status as value, status as label FROM sales_order_mng ${statusWhere} ORDER BY status`, filterParams ), ]); // 품목은 데이터에서 추출 (실제 수주에 사용된 품목만) const itemSet = new Map(); dataRows.forEach((row: any) => { if (row.part_code && !itemSet.has(row.part_code)) { itemSet.set(row.part_code, row.item); } }); const items = Array.from(itemSet.entries()).map(([value, label]) => ({ value, label, })); logger.info("영업 리포트 데이터 조회", { companyCode, rowCount: dataRows.length, startDate, endDate, }); res.status(200).json({ success: true, data: { rows: dataRows, filterOptions: { customers: customersRows, items, statuses: statusRows, }, totalCount: dataRows.length, }, }); } catch (error: any) { logger.error("영업 리포트 데이터 조회 실패", { error: error.message, stack: error.stack, }); res.status(500).json({ success: false, message: "영업 리포트 데이터 조회에 실패했습니다", error: error.message, }); } }