feat: enhance design task management page with employee selection and filtering
- Integrated a combobox for selecting employees, allowing users to assign tasks more efficiently. - Implemented fetching of employee data to populate the selection options, improving user experience. - Added functionality to filter tasks based on the current user's related tasks, enhancing task management capabilities. - Updated the modal states for better handling of designer assignments and task interactions. These changes aim to streamline the design task management process, facilitating better organization and assignment of tasks within the application.
This commit is contained in:
352
backend-node/src/controllers/materialStatusController.ts
Normal file
352
backend-node/src/controllers/materialStatusController.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* 자재현황 컨트롤러
|
||||
* - 생산계획(작업지시) 조회
|
||||
* - 선택된 작업지시의 BOM 기반 자재소요량 + 재고 현황 조회
|
||||
* - 창고 목록 조회
|
||||
*/
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { pool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// ─── 생산계획(작업지시) 조회 ───
|
||||
|
||||
export async function getWorkOrders(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { dateFrom, dateTo, itemCode, itemName } = req.query;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (companyCode === "*") {
|
||||
logger.info("최고 관리자 전체 작업지시 조회");
|
||||
} else {
|
||||
conditions.push(`p.company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
conditions.push(`p.plan_date >= $${paramIndex}::date`);
|
||||
params.push(dateFrom);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (dateTo) {
|
||||
conditions.push(`p.plan_date <= $${paramIndex}::date`);
|
||||
params.push(dateTo);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (itemCode) {
|
||||
conditions.push(`p.item_code ILIKE $${paramIndex}`);
|
||||
params.push(`%${itemCode}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (itemName) {
|
||||
conditions.push(`p.item_name ILIKE $${paramIndex}`);
|
||||
params.push(`%${itemName}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
p.id,
|
||||
p.plan_no,
|
||||
p.item_code,
|
||||
p.item_name,
|
||||
p.plan_qty,
|
||||
p.completed_qty,
|
||||
p.plan_date,
|
||||
p.start_date,
|
||||
p.end_date,
|
||||
p.status,
|
||||
p.work_order_no,
|
||||
p.company_code
|
||||
FROM production_plan_mng p
|
||||
${whereClause}
|
||||
ORDER BY p.plan_date DESC, p.created_date DESC
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("작업지시 조회 완료", {
|
||||
companyCode,
|
||||
rowCount: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("작업지시 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 선택된 작업지시의 자재소요 + 재고 현황 조회 ───
|
||||
|
||||
export async function getMaterialStatus(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { planIds, warehouseCode } = req.body;
|
||||
|
||||
if (!planIds || !Array.isArray(planIds) || planIds.length === 0) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "작업지시를 선택해주세요." });
|
||||
}
|
||||
|
||||
// 1) 선택된 작업지시의 품목코드 + 수량 조회
|
||||
const planPlaceholders = planIds
|
||||
.map((_, i) => `$${i + 1}`)
|
||||
.join(",");
|
||||
let paramIndex = planIds.length + 1;
|
||||
|
||||
const companyCondition =
|
||||
companyCode === "*" ? "" : `AND p.company_code = $${paramIndex}`;
|
||||
const planParams: any[] = [...planIds];
|
||||
if (companyCode !== "*") {
|
||||
planParams.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const planQuery = `
|
||||
SELECT p.item_code, p.item_name, p.plan_qty
|
||||
FROM production_plan_mng p
|
||||
WHERE p.id IN (${planPlaceholders})
|
||||
${companyCondition}
|
||||
`;
|
||||
|
||||
const planResult = await pool.query(planQuery, planParams);
|
||||
|
||||
if (planResult.rowCount === 0) {
|
||||
return res.json({ success: true, data: [] });
|
||||
}
|
||||
|
||||
// 2) 해당 품목들의 BOM에서 필요 자재 목록 조회
|
||||
const itemCodes = planResult.rows.map((r: any) => r.item_code);
|
||||
const planQtyMap: Record<string, number> = {};
|
||||
for (const row of planResult.rows) {
|
||||
const code = row.item_code;
|
||||
planQtyMap[code] = (planQtyMap[code] || 0) + Number(row.plan_qty || 0);
|
||||
}
|
||||
|
||||
const itemPlaceholders = itemCodes.map((_: any, i: number) => `$${i + 1}`).join(",");
|
||||
|
||||
// BOM 조인: bom -> bom_detail -> item_info (자재 정보)
|
||||
const bomCompanyCondition =
|
||||
companyCode === "*" ? "" : `AND b.company_code = $${itemCodes.length + 1}`;
|
||||
const bomParams: any[] = [...itemCodes];
|
||||
if (companyCode !== "*") {
|
||||
bomParams.push(companyCode);
|
||||
}
|
||||
|
||||
const bomQuery = `
|
||||
SELECT
|
||||
b.item_code AS parent_item_code,
|
||||
b.base_qty AS bom_base_qty,
|
||||
bd.child_item_id,
|
||||
bd.quantity AS bom_qty,
|
||||
bd.unit AS bom_unit,
|
||||
bd.loss_rate,
|
||||
ii.item_name AS material_name,
|
||||
ii.item_number AS material_code,
|
||||
ii.unit AS material_unit
|
||||
FROM bom b
|
||||
JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code
|
||||
LEFT JOIN item_info ii ON bd.child_item_id = ii.id AND b.company_code = ii.company_code
|
||||
WHERE b.item_code IN (${itemPlaceholders})
|
||||
${bomCompanyCondition}
|
||||
ORDER BY b.item_code, bd.seq_no
|
||||
`;
|
||||
|
||||
const bomResult = await pool.query(bomQuery, bomParams);
|
||||
|
||||
// 3) 자재별 필요수량 계산
|
||||
interface MaterialNeed {
|
||||
childItemId: string;
|
||||
materialCode: string;
|
||||
materialName: string;
|
||||
unit: string;
|
||||
requiredQty: number;
|
||||
}
|
||||
|
||||
const materialMap: Record<string, MaterialNeed> = {};
|
||||
|
||||
for (const bomRow of bomResult.rows) {
|
||||
const parentQty = planQtyMap[bomRow.parent_item_code] || 0;
|
||||
const baseQty = Number(bomRow.bom_base_qty) || 1;
|
||||
const bomQty = Number(bomRow.bom_qty) || 0;
|
||||
const lossRate = Number(bomRow.loss_rate) || 0;
|
||||
|
||||
// 필요수량 = (생산수량 / BOM기준수량) * BOM자재수량 * (1 + 로스율/100)
|
||||
const requiredQty =
|
||||
(parentQty / baseQty) * bomQty * (1 + lossRate / 100);
|
||||
|
||||
const key = bomRow.child_item_id;
|
||||
if (materialMap[key]) {
|
||||
materialMap[key].requiredQty += requiredQty;
|
||||
} else {
|
||||
materialMap[key] = {
|
||||
childItemId: bomRow.child_item_id,
|
||||
materialCode:
|
||||
bomRow.material_code || bomRow.child_item_id,
|
||||
materialName: bomRow.material_name || "알 수 없음",
|
||||
unit: bomRow.bom_unit || bomRow.material_unit || "EA",
|
||||
requiredQty,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const materialIds = Object.keys(materialMap);
|
||||
|
||||
if (materialIds.length === 0) {
|
||||
return res.json({ success: true, data: [] });
|
||||
}
|
||||
|
||||
// 4) 재고 조회 (창고/위치별)
|
||||
const stockPlaceholders = materialIds
|
||||
.map((_, i) => `$${i + 1}`)
|
||||
.join(",");
|
||||
const stockParams: any[] = [...materialIds];
|
||||
let stockParamIdx = materialIds.length + 1;
|
||||
|
||||
const stockConditions: string[] = [
|
||||
`s.item_code IN (${stockPlaceholders})`,
|
||||
];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
stockConditions.push(`s.company_code = $${stockParamIdx}`);
|
||||
stockParams.push(companyCode);
|
||||
stockParamIdx++;
|
||||
}
|
||||
|
||||
if (warehouseCode) {
|
||||
stockConditions.push(`s.warehouse_code = $${stockParamIdx}`);
|
||||
stockParams.push(warehouseCode);
|
||||
stockParamIdx++;
|
||||
}
|
||||
|
||||
const stockQuery = `
|
||||
SELECT
|
||||
s.item_code,
|
||||
s.warehouse_code,
|
||||
s.location_code,
|
||||
COALESCE(CAST(s.current_qty AS NUMERIC), 0) AS current_qty
|
||||
FROM inventory_stock s
|
||||
WHERE ${stockConditions.join(" AND ")}
|
||||
AND COALESCE(CAST(s.current_qty AS NUMERIC), 0) > 0
|
||||
ORDER BY s.item_code, s.warehouse_code, s.location_code
|
||||
`;
|
||||
|
||||
const stockResult = await pool.query(stockQuery, stockParams);
|
||||
|
||||
// 5) 결과 조합
|
||||
// item_code 기준 재고 맵핑 (inventory_stock.item_code는 item_info.item_number 또는 item_info.id일 수 있음)
|
||||
const stockByItem: Record<
|
||||
string,
|
||||
{ location: string; warehouse: string; qty: number }[]
|
||||
> = {};
|
||||
|
||||
for (const stockRow of stockResult.rows) {
|
||||
const code = stockRow.item_code;
|
||||
if (!stockByItem[code]) {
|
||||
stockByItem[code] = [];
|
||||
}
|
||||
stockByItem[code].push({
|
||||
location: stockRow.location_code || "",
|
||||
warehouse: stockRow.warehouse_code || "",
|
||||
qty: Number(stockRow.current_qty),
|
||||
});
|
||||
}
|
||||
|
||||
const resultData = materialIds.map((id) => {
|
||||
const material = materialMap[id];
|
||||
// inventory_stock의 item_code가 item_number 또는 child_item_id일 수 있음
|
||||
const locations =
|
||||
stockByItem[material.materialCode] ||
|
||||
stockByItem[id] ||
|
||||
[];
|
||||
|
||||
const totalCurrentQty = locations.reduce(
|
||||
(sum, loc) => sum + loc.qty,
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
code: material.materialCode,
|
||||
name: material.materialName,
|
||||
required: Math.round(material.requiredQty * 100) / 100,
|
||||
current: totalCurrentQty,
|
||||
unit: material.unit,
|
||||
locations,
|
||||
};
|
||||
});
|
||||
|
||||
logger.info("자재현황 조회 완료", {
|
||||
companyCode,
|
||||
planCount: planIds.length,
|
||||
materialCount: resultData.length,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: resultData });
|
||||
} catch (error: any) {
|
||||
logger.error("자재현황 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 창고 목록 조회 ───
|
||||
|
||||
export async function getWarehouses(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
query = `
|
||||
SELECT DISTINCT warehouse_code, warehouse_name, warehouse_type
|
||||
FROM warehouse_info
|
||||
ORDER BY warehouse_code
|
||||
`;
|
||||
params = [];
|
||||
} else {
|
||||
query = `
|
||||
SELECT DISTINCT warehouse_code, warehouse_name, warehouse_type
|
||||
FROM warehouse_info
|
||||
WHERE company_code = $1
|
||||
ORDER BY warehouse_code
|
||||
`;
|
||||
params = [companyCode];
|
||||
}
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("창고 목록 조회 완료", {
|
||||
companyCode,
|
||||
rowCount: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("창고 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user