feat: enhance application with receiving management and global font size adjustments
- Added new route for receiving management, allowing users to manage incoming goods effectively. - Updated global CSS to unify font sizes across the application, ensuring a consistent user experience. - Modified customer and sales order fetching logic to improve data retrieval and handling, including enhanced error logging for better debugging. These changes aim to streamline the receiving process and improve overall UI consistency within the application.
This commit is contained in:
parent
a5eba3a4ca
commit
8c29225043
|
|
@ -150,6 +150,7 @@ import salesReportRoutes from "./routes/salesReportRoutes"; // 영업 리포트
|
|||
import analyticsReportRoutes from "./routes/analyticsReportRoutes"; // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
||||
import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프로젝트/ECN)
|
||||
import materialStatusRoutes from "./routes/materialStatusRoutes"; // 자재현황
|
||||
import receivingRoutes from "./routes/receivingRoutes"; // 입고관리
|
||||
import processInfoRoutes from "./routes/processInfoRoutes"; // 공정정보관리
|
||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
|
|
@ -351,6 +352,7 @@ app.use("/api/work-instruction", workInstructionRoutes); // 작업지시 관리
|
|||
app.use("/api/sales-report", salesReportRoutes); // 영업 리포트
|
||||
app.use("/api/report", analyticsReportRoutes); // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
||||
app.use("/api/design", designRoutes); // 설계 모듈
|
||||
app.use("/api/receiving", receivingRoutes); // 입고관리
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||
|
|
|
|||
|
|
@ -0,0 +1,487 @@
|
|||
/**
|
||||
* 입고관리 컨트롤러
|
||||
*
|
||||
* 입고유형별 소스 테이블:
|
||||
* - 구매입고 → purchase_order_mng (발주)
|
||||
* - 반품입고 → shipment_instruction + shipment_instruction_detail (출하)
|
||||
* - 기타입고 → item_info (품목)
|
||||
*/
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// 입고 목록 조회
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const {
|
||||
inbound_type,
|
||||
inbound_status,
|
||||
search_keyword,
|
||||
date_from,
|
||||
date_to,
|
||||
} = req.query;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 전체 조회
|
||||
} else {
|
||||
conditions.push(`im.company_code = $${paramIdx}`);
|
||||
params.push(companyCode);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (inbound_type && inbound_type !== "all") {
|
||||
conditions.push(`im.inbound_type = $${paramIdx}`);
|
||||
params.push(inbound_type);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (inbound_status && inbound_status !== "all") {
|
||||
conditions.push(`im.inbound_status = $${paramIdx}`);
|
||||
params.push(inbound_status);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (search_keyword) {
|
||||
conditions.push(
|
||||
`(im.inbound_number ILIKE $${paramIdx} OR im.item_name ILIKE $${paramIdx} OR im.item_number ILIKE $${paramIdx} OR im.supplier_name ILIKE $${paramIdx} OR im.reference_number ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${search_keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (date_from) {
|
||||
conditions.push(`im.inbound_date >= $${paramIdx}::date`);
|
||||
params.push(date_from);
|
||||
paramIdx++;
|
||||
}
|
||||
if (date_to) {
|
||||
conditions.push(`im.inbound_date <= $${paramIdx}::date`);
|
||||
params.push(date_to);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
im.*,
|
||||
wh.warehouse_name
|
||||
FROM inbound_mng im
|
||||
LEFT JOIN warehouse_info wh
|
||||
ON im.warehouse_code = wh.warehouse_code
|
||||
AND im.company_code = wh.company_code
|
||||
${whereClause}
|
||||
ORDER BY im.created_date DESC
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
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 create(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { items, inbound_number, inbound_date, warehouse_code, location_code, inspector, manager, memo } = req.body;
|
||||
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "입고 품목이 없습니다." });
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
const insertedRows: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const result = await client.query(
|
||||
`INSERT INTO inbound_mng (
|
||||
company_code, inbound_number, inbound_type, inbound_date,
|
||||
reference_number, supplier_code, supplier_name,
|
||||
item_number, item_name, spec, material, unit,
|
||||
inbound_qty, unit_price, total_amount,
|
||||
lot_number, warehouse_code, location_code,
|
||||
inbound_status, inspection_status,
|
||||
inspector, manager, memo,
|
||||
source_table, source_id,
|
||||
created_date, created_by, writer, status
|
||||
) VALUES (
|
||||
$1, $2, $3, $4::date,
|
||||
$5, $6, $7,
|
||||
$8, $9, $10, $11, $12,
|
||||
$13, $14, $15,
|
||||
$16, $17, $18,
|
||||
$19, $20,
|
||||
$21, $22, $23,
|
||||
$24, $25,
|
||||
NOW(), $26, $26, '입고'
|
||||
) RETURNING *`,
|
||||
[
|
||||
companyCode,
|
||||
inbound_number || item.inbound_number,
|
||||
item.inbound_type,
|
||||
inbound_date || item.inbound_date,
|
||||
item.reference_number || null,
|
||||
item.supplier_code || null,
|
||||
item.supplier_name || null,
|
||||
item.item_number || null,
|
||||
item.item_name || null,
|
||||
item.spec || null,
|
||||
item.material || null,
|
||||
item.unit || "EA",
|
||||
item.inbound_qty || 0,
|
||||
item.unit_price || 0,
|
||||
item.total_amount || 0,
|
||||
item.lot_number || null,
|
||||
warehouse_code || item.warehouse_code || null,
|
||||
location_code || item.location_code || null,
|
||||
item.inbound_status || "대기",
|
||||
item.inspection_status || "대기",
|
||||
inspector || item.inspector || null,
|
||||
manager || item.manager || null,
|
||||
memo || item.memo || null,
|
||||
item.source_table || null,
|
||||
item.source_id || null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
insertedRows.push(result.rows[0]);
|
||||
|
||||
// 구매입고인 경우 발주의 received_qty 업데이트
|
||||
if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_order_mng") {
|
||||
await client.query(
|
||||
`UPDATE purchase_order_mng
|
||||
SET received_qty = CAST(
|
||||
COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1 AS text
|
||||
),
|
||||
remain_qty = CAST(
|
||||
GREATEST(COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
|
||||
- COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) - $1, 0) AS text
|
||||
),
|
||||
status = CASE
|
||||
WHEN COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1
|
||||
>= COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
|
||||
THEN '입고완료'
|
||||
ELSE '부분입고'
|
||||
END,
|
||||
updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[item.inbound_qty || 0, item.source_id, companyCode]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("입고 등록 완료", {
|
||||
companyCode,
|
||||
userId,
|
||||
count: insertedRows.length,
|
||||
inbound_number,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: insertedRows,
|
||||
message: `${insertedRows.length}건 입고 등록 완료`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("입고 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 입고 수정
|
||||
export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const {
|
||||
inbound_date, inbound_qty, unit_price, total_amount,
|
||||
lot_number, warehouse_code, location_code,
|
||||
inbound_status, inspection_status,
|
||||
inspector, manager: mgr, memo,
|
||||
} = req.body;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`UPDATE inbound_mng SET
|
||||
inbound_date = COALESCE($1::date, inbound_date),
|
||||
inbound_qty = COALESCE($2, inbound_qty),
|
||||
unit_price = COALESCE($3, unit_price),
|
||||
total_amount = COALESCE($4, total_amount),
|
||||
lot_number = COALESCE($5, lot_number),
|
||||
warehouse_code = COALESCE($6, warehouse_code),
|
||||
location_code = COALESCE($7, location_code),
|
||||
inbound_status = COALESCE($8, inbound_status),
|
||||
inspection_status = COALESCE($9, inspection_status),
|
||||
inspector = COALESCE($10, inspector),
|
||||
manager = COALESCE($11, manager),
|
||||
memo = COALESCE($12, memo),
|
||||
updated_date = NOW(),
|
||||
updated_by = $13
|
||||
WHERE id = $14 AND company_code = $15
|
||||
RETURNING *`,
|
||||
[
|
||||
inbound_date, inbound_qty, unit_price, total_amount,
|
||||
lot_number, warehouse_code, location_code,
|
||||
inbound_status, inspection_status,
|
||||
inspector, mgr, memo,
|
||||
userId, id, companyCode,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, message: "입고 데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
logger.info("입고 수정", { companyCode, userId, id });
|
||||
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("입고 수정 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 입고 삭제
|
||||
export async function deleteReceiving(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`DELETE FROM inbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
logger.info("입고 삭제", { companyCode, id });
|
||||
|
||||
return res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
logger.error("입고 삭제 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 구매입고용: 발주 데이터 조회 (미입고분)
|
||||
export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
// 잔량이 있는 것만 조회
|
||||
conditions.push(
|
||||
`COALESCE(CAST(NULLIF(remain_qty, '') AS numeric), COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0)) > 0`
|
||||
);
|
||||
conditions.push(`status NOT IN ('입고완료', '취소')`);
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, purchase_no, order_date, supplier_code, supplier_name,
|
||||
item_code, item_name, spec, material,
|
||||
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) AS order_qty,
|
||||
COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) AS received_qty,
|
||||
COALESCE(CAST(NULLIF(remain_qty, '') AS numeric),
|
||||
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
|
||||
- COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0)
|
||||
) AS remain_qty,
|
||||
COALESCE(CAST(NULLIF(unit_price, '') AS numeric), 0) AS unit_price,
|
||||
status, due_date
|
||||
FROM purchase_order_mng
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY order_date DESC, purchase_no`,
|
||||
params
|
||||
);
|
||||
|
||||
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 getShipments(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const conditions: string[] = ["si.company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
sid.id AS detail_id,
|
||||
si.id AS instruction_id,
|
||||
si.instruction_no,
|
||||
si.instruction_date,
|
||||
si.partner_id,
|
||||
si.status AS instruction_status,
|
||||
sid.item_code,
|
||||
sid.item_name,
|
||||
sid.spec,
|
||||
sid.material,
|
||||
COALESCE(sid.ship_qty, 0) AS ship_qty,
|
||||
COALESCE(sid.order_qty, 0) AS order_qty,
|
||||
sid.source_type
|
||||
FROM shipment_instruction si
|
||||
JOIN shipment_instruction_detail sid
|
||||
ON si.id = sid.instruction_id
|
||||
AND si.company_code = sid.company_code
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY si.instruction_date DESC, si.instruction_no`,
|
||||
params
|
||||
);
|
||||
|
||||
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 getItems(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, item_number, item_name, size AS spec, material, unit,
|
||||
COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price
|
||||
FROM item_info
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY item_name`,
|
||||
params
|
||||
);
|
||||
|
||||
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 generateNumber(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const pool = getPool();
|
||||
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const prefix = `RCV-${yyyy}-`;
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT inbound_number FROM inbound_mng
|
||||
WHERE company_code = $1 AND inbound_number LIKE $2
|
||||
ORDER BY inbound_number DESC LIMIT 1`,
|
||||
[companyCode, `${prefix}%`]
|
||||
);
|
||||
|
||||
let seq = 1;
|
||||
if (result.rows.length > 0) {
|
||||
const lastNo = result.rows[0].inbound_number;
|
||||
const lastSeq = parseInt(lastNo.replace(prefix, ""), 10);
|
||||
if (!isNaN(lastSeq)) seq = lastSeq + 1;
|
||||
}
|
||||
|
||||
const newNumber = `${prefix}${String(seq).padStart(4, "0")}`;
|
||||
|
||||
return res.json({ success: true, data: newNumber });
|
||||
} 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;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT warehouse_code, warehouse_name, warehouse_type
|
||||
FROM warehouse_info
|
||||
WHERE company_code = $1 AND status != '삭제'
|
||||
ORDER BY warehouse_name`,
|
||||
[companyCode]
|
||||
);
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 입고관리 라우트
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import * as receivingController from "../controllers/receivingController";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
|
||||
// 입고 목록 조회
|
||||
router.get("/list", receivingController.getList);
|
||||
|
||||
// 입고번호 자동생성
|
||||
router.get("/generate-number", receivingController.generateNumber);
|
||||
|
||||
// 창고 목록 조회
|
||||
router.get("/warehouses", receivingController.getWarehouses);
|
||||
|
||||
// 소스 데이터: 발주 (구매입고)
|
||||
router.get("/source/purchase-orders", receivingController.getPurchaseOrders);
|
||||
|
||||
// 소스 데이터: 출하 (반품입고)
|
||||
router.get("/source/shipments", receivingController.getShipments);
|
||||
|
||||
// 소스 데이터: 품목 (기타입고)
|
||||
router.get("/source/items", receivingController.getItems);
|
||||
|
||||
// 입고 등록
|
||||
router.post("/", receivingController.create);
|
||||
|
||||
// 입고 수정
|
||||
router.put("/:id", receivingController.update);
|
||||
|
||||
// 입고 삭제
|
||||
router.delete("/:id", receivingController.deleteReceiving);
|
||||
|
||||
export default router;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -212,21 +212,24 @@ export default function ClaimManagementPage() {
|
|||
}, []);
|
||||
|
||||
// 거래처 목록 조회
|
||||
const fetchCustomers = useCallback(async () => {
|
||||
if (customers.length > 0) return;
|
||||
const fetchCustomers = useCallback(async (force = false) => {
|
||||
if (!force && customers.length > 0) return;
|
||||
setCustomersLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/customer_mng/data", {
|
||||
page: 1,
|
||||
size: 9999,
|
||||
autoFilter: true,
|
||||
autoFilter: { enabled: true },
|
||||
});
|
||||
if (res.data?.success && res.data?.data?.rows) {
|
||||
const list: CustomerOption[] = res.data.data.rows.map((row: any) => ({
|
||||
customerCode: row.customer_code || "",
|
||||
const rows = res.data?.data?.data || res.data?.data?.rows;
|
||||
if (res.data?.success && Array.isArray(rows)) {
|
||||
const list: CustomerOption[] = rows.map((row: any) => ({
|
||||
customerCode: row.customer_code || row.id || "",
|
||||
customerName: row.customer_name || "",
|
||||
}));
|
||||
setCustomers(list);
|
||||
} else {
|
||||
console.warn("거래처 응답 구조 확인:", JSON.stringify(res.data, null, 2));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("거래처 목록 조회 실패:", e);
|
||||
|
|
@ -236,19 +239,20 @@ export default function ClaimManagementPage() {
|
|||
}, [customers.length]);
|
||||
|
||||
// 수주 목록 조회
|
||||
const fetchSalesOrders = useCallback(async () => {
|
||||
if (salesOrders.length > 0) return;
|
||||
const fetchSalesOrders = useCallback(async (force = false) => {
|
||||
if (!force && salesOrders.length > 0) return;
|
||||
setOrdersLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/sales_order_mng/data", {
|
||||
page: 1,
|
||||
size: 9999,
|
||||
autoFilter: true,
|
||||
autoFilter: { enabled: true },
|
||||
});
|
||||
if (res.data?.success && res.data?.data?.rows) {
|
||||
const rows = res.data?.data?.data || res.data?.data?.rows;
|
||||
if (res.data?.success && Array.isArray(rows)) {
|
||||
const seen = new Set<string>();
|
||||
const list: SalesOrderOption[] = [];
|
||||
for (const row of res.data.data.rows) {
|
||||
for (const row of rows) {
|
||||
const orderNo = row.order_no || "";
|
||||
if (!orderNo || seen.has(orderNo)) continue;
|
||||
seen.add(orderNo);
|
||||
|
|
@ -259,6 +263,8 @@ export default function ClaimManagementPage() {
|
|||
});
|
||||
}
|
||||
setSalesOrders(list);
|
||||
} else {
|
||||
console.warn("수주 응답 구조 확인:", JSON.stringify(res.data, null, 2));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("수주 목록 조회 실패:", e);
|
||||
|
|
@ -343,6 +349,8 @@ export default function ClaimManagementPage() {
|
|||
processContent: "",
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
fetchCustomers(true);
|
||||
fetchSalesOrders(true);
|
||||
};
|
||||
|
||||
const openEditModal = (claimNo: string) => {
|
||||
|
|
@ -351,6 +359,8 @@ export default function ClaimManagementPage() {
|
|||
setIsEditMode(true);
|
||||
setFormData({ ...claim });
|
||||
setIsModalOpen(true);
|
||||
fetchCustomers(true);
|
||||
fetchSalesOrders(true);
|
||||
};
|
||||
|
||||
const handleFormChange = (field: keyof Claim, value: string) => {
|
||||
|
|
@ -893,7 +903,7 @@ export default function ClaimManagementPage() {
|
|||
role="combobox"
|
||||
aria-expanded={customerOpen}
|
||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal"
|
||||
onClick={() => fetchCustomers()}
|
||||
onClick={() => fetchCustomers(false)}
|
||||
>
|
||||
{formData.customerName || "거래처 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
|
|
@ -985,7 +995,7 @@ export default function ClaimManagementPage() {
|
|||
role="combobox"
|
||||
aria-expanded={orderOpen}
|
||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal"
|
||||
onClick={() => fetchSalesOrders()}
|
||||
onClick={() => fetchSalesOrders(false)}
|
||||
>
|
||||
{formData.orderNo || "수주번호 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
|
|
|
|||
|
|
@ -124,6 +124,23 @@ export default function ShippingPlanPage() {
|
|||
|
||||
const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]);
|
||||
|
||||
const groupedData = useMemo(() => {
|
||||
const orderMap = new Map<string, ShipmentPlanListItem[]>();
|
||||
const orderKeys: string[] = [];
|
||||
data.forEach(plan => {
|
||||
const key = plan.order_no || `_no_order_${plan.id}`;
|
||||
if (!orderMap.has(key)) {
|
||||
orderMap.set(key, []);
|
||||
orderKeys.push(key);
|
||||
}
|
||||
orderMap.get(key)!.push(plan);
|
||||
});
|
||||
return orderKeys.map(key => ({
|
||||
orderNo: key,
|
||||
plans: orderMap.get(key)!,
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
const handleRowClick = (plan: ShipmentPlanListItem) => {
|
||||
if (isDetailChanged && selectedId !== plan.id) {
|
||||
if (!confirm("변경사항이 있습니다. 저장하지 않고 이동하시겠습니까?")) return;
|
||||
|
|
@ -298,59 +315,73 @@ export default function ShippingPlanPage() {
|
|||
onCheckedChange={handleCheckAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-[50px] text-center">No</TableHead>
|
||||
<TableHead className="w-[80px]">상태</TableHead>
|
||||
<TableHead className="w-[140px]">수주번호</TableHead>
|
||||
<TableHead className="w-[160px]">수주번호</TableHead>
|
||||
<TableHead className="w-[100px] text-center">납기일</TableHead>
|
||||
<TableHead className="w-[120px]">거래처</TableHead>
|
||||
<TableHead className="w-[100px]">품목코드</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
<TableHead className="w-[80px] text-right">수주수량</TableHead>
|
||||
<TableHead className="w-[80px] text-right">계획수량</TableHead>
|
||||
<TableHead className="w-[100px] text-center">출하계획일</TableHead>
|
||||
<TableHead className="w-[100px] text-center">납기일</TableHead>
|
||||
<TableHead className="w-[80px] text-center">상태</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.length === 0 ? (
|
||||
{groupedData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} className="h-32 text-center text-muted-foreground">
|
||||
<TableCell colSpan={10} className="h-32 text-center text-muted-foreground">
|
||||
출하계획이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((plan, idx) => (
|
||||
<TableRow
|
||||
key={plan.id}
|
||||
className={cn(
|
||||
"cursor-pointer hover:bg-muted/50 transition-colors",
|
||||
selectedId === plan.id && "bg-primary/5",
|
||||
plan.status === "CANCELLED" && "opacity-60 bg-slate-50"
|
||||
)}
|
||||
onClick={() => handleRowClick(plan)}
|
||||
>
|
||||
<TableCell className="text-center" onClick={e => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={checkedIds.includes(plan.id)}
|
||||
onCheckedChange={(c) => handleCheck(plan.id, c as boolean)}
|
||||
disabled={plan.status === "CANCELLED"}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
|
||||
<TableCell>
|
||||
<span className={cn("px-2 py-1 rounded-full text-[11px] font-medium border", getStatusColor(plan.status))}>
|
||||
{getStatusLabel(plan.status)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{plan.order_no || "-"}</TableCell>
|
||||
<TableCell>{plan.customer_name || "-"}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">{plan.part_code || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{plan.part_name || "-"}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(plan.order_qty)}</TableCell>
|
||||
<TableCell className="text-right font-semibold text-primary">{formatNumber(plan.plan_qty)}</TableCell>
|
||||
<TableCell className="text-center">{formatDate(plan.plan_date)}</TableCell>
|
||||
<TableCell className="text-center">{formatDate(plan.due_date)}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
groupedData.map((group) =>
|
||||
group.plans.map((plan, planIdx) => (
|
||||
<TableRow
|
||||
key={plan.id}
|
||||
className={cn(
|
||||
"cursor-pointer hover:bg-muted/50 transition-colors",
|
||||
selectedId === plan.id && "bg-primary/5",
|
||||
plan.status === "CANCELLED" && "opacity-60 bg-slate-50",
|
||||
planIdx === 0 && "border-t-2 border-t-border"
|
||||
)}
|
||||
onClick={() => handleRowClick(plan)}
|
||||
>
|
||||
<TableCell className="text-center" onClick={e => e.stopPropagation()}>
|
||||
{planIdx === 0 && (
|
||||
<Checkbox
|
||||
checked={group.plans.every(p => checkedIds.includes(p.id))}
|
||||
onCheckedChange={(c) => {
|
||||
if (c) {
|
||||
setCheckedIds(prev => [...new Set([...prev, ...group.plans.filter(p => p.status !== "CANCELLED").map(p => p.id)])]);
|
||||
} else {
|
||||
setCheckedIds(prev => prev.filter(id => !group.plans.some(p => p.id === id)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{planIdx === 0 ? (plan.order_no || "-") : ""}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{planIdx === 0 ? formatDate(plan.due_date) : ""}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{planIdx === 0 ? (plan.customer_name || "-") : ""}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">{plan.part_code || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{plan.part_name || "-"}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(plan.order_qty)}</TableCell>
|
||||
<TableCell className="text-right font-semibold text-primary">{formatNumber(plan.plan_qty)}</TableCell>
|
||||
<TableCell className="text-center">{formatDate(plan.plan_date)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className={cn("px-2 py-1 rounded-full text-[11px] font-medium border", getStatusColor(plan.status))}>
|
||||
{getStatusLabel(plan.status)}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
@ -390,10 +421,6 @@ export default function ShippingPlanPage() {
|
|||
<section>
|
||||
<h3 className="text-sm font-semibold mb-3">기본 정보</h3>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs block mb-1">출하계획번호</span>
|
||||
<span className="font-medium">{selectedPlan.shipment_plan_no || "-"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs block mb-1">상태</span>
|
||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border inline-block", getStatusColor(selectedPlan.status))}>
|
||||
|
|
|
|||
|
|
@ -508,6 +508,16 @@ select {
|
|||
font-family: "Gaegu", cursive;
|
||||
}
|
||||
|
||||
/* ===== 전체 폰트 사이즈 16px 통일 (버튼 제외) ===== */
|
||||
body *:not(button, [role="button"]) {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
body button *,
|
||||
body [role="button"] * {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
/* ===== Component-Specific Overrides ===== */
|
||||
/* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */
|
||||
/* 예: Calendar, Table 등의 미세 조정 */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
import { apiClient } from "./client";
|
||||
|
||||
// --- 타입 정의 ---
|
||||
|
||||
export interface InboundItem {
|
||||
id: string;
|
||||
company_code: string;
|
||||
inbound_number: string;
|
||||
inbound_type: string;
|
||||
inbound_date: string;
|
||||
reference_number: string | null;
|
||||
supplier_code: string | null;
|
||||
supplier_name: string | null;
|
||||
item_number: string | null;
|
||||
item_name: string | null;
|
||||
spec: string | null;
|
||||
material: string | null;
|
||||
unit: string | null;
|
||||
inbound_qty: number;
|
||||
unit_price: number;
|
||||
total_amount: number;
|
||||
lot_number: string | null;
|
||||
warehouse_code: string | null;
|
||||
warehouse_name?: string | null;
|
||||
location_code: string | null;
|
||||
inbound_status: string;
|
||||
inspection_status: string | null;
|
||||
inspector: string | null;
|
||||
manager: string | null;
|
||||
memo: string | null;
|
||||
source_table: string | null;
|
||||
source_id: string | null;
|
||||
created_date: string;
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderSource {
|
||||
id: string;
|
||||
purchase_no: string;
|
||||
order_date: string;
|
||||
supplier_code: string;
|
||||
supplier_name: string;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
spec: string | null;
|
||||
material: string | null;
|
||||
order_qty: number;
|
||||
received_qty: number;
|
||||
remain_qty: number;
|
||||
unit_price: number;
|
||||
status: string;
|
||||
due_date: string | null;
|
||||
}
|
||||
|
||||
export interface ShipmentSource {
|
||||
detail_id: number;
|
||||
instruction_id: number;
|
||||
instruction_no: string;
|
||||
instruction_date: string;
|
||||
partner_id: string;
|
||||
instruction_status: string;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
spec: string | null;
|
||||
material: string | null;
|
||||
ship_qty: number;
|
||||
order_qty: number;
|
||||
source_type: string | null;
|
||||
}
|
||||
|
||||
export interface ItemSource {
|
||||
id: string;
|
||||
item_number: string;
|
||||
item_name: string;
|
||||
spec: string | null;
|
||||
material: string | null;
|
||||
unit: string | null;
|
||||
standard_price: number;
|
||||
}
|
||||
|
||||
export interface WarehouseOption {
|
||||
warehouse_code: string;
|
||||
warehouse_name: string;
|
||||
warehouse_type: string;
|
||||
}
|
||||
|
||||
export interface CreateReceivingPayload {
|
||||
inbound_number: string;
|
||||
inbound_date: string;
|
||||
warehouse_code?: string;
|
||||
location_code?: string;
|
||||
inspector?: string;
|
||||
manager?: string;
|
||||
memo?: string;
|
||||
items: Array<{
|
||||
inbound_type: string;
|
||||
reference_number?: string;
|
||||
supplier_code?: string;
|
||||
supplier_name?: string;
|
||||
item_number?: string;
|
||||
item_name?: string;
|
||||
spec?: string;
|
||||
material?: string;
|
||||
unit?: string;
|
||||
inbound_qty: number;
|
||||
unit_price?: number;
|
||||
total_amount?: number;
|
||||
lot_number?: string;
|
||||
warehouse_code?: string;
|
||||
location_code?: string;
|
||||
inbound_status?: string;
|
||||
inspection_status?: string;
|
||||
inspector?: string;
|
||||
manager?: string;
|
||||
memo?: string;
|
||||
source_table: string;
|
||||
source_id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// --- API 호출 ---
|
||||
|
||||
export async function getReceivingList(params?: {
|
||||
inbound_type?: string;
|
||||
inbound_status?: string;
|
||||
search_keyword?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}) {
|
||||
const res = await apiClient.get("/receiving/list", { params });
|
||||
return res.data as { success: boolean; data: InboundItem[] };
|
||||
}
|
||||
|
||||
export async function createReceiving(payload: CreateReceivingPayload) {
|
||||
const res = await apiClient.post("/receiving", payload);
|
||||
return res.data as { success: boolean; data: InboundItem[]; message?: string };
|
||||
}
|
||||
|
||||
export async function updateReceiving(id: string, payload: Partial<InboundItem>) {
|
||||
const res = await apiClient.put(`/receiving/${id}`, payload);
|
||||
return res.data as { success: boolean; data: InboundItem };
|
||||
}
|
||||
|
||||
export async function deleteReceiving(id: string) {
|
||||
const res = await apiClient.delete(`/receiving/${id}`);
|
||||
return res.data as { success: boolean; message?: string };
|
||||
}
|
||||
|
||||
export async function generateReceivingNumber() {
|
||||
const res = await apiClient.get("/receiving/generate-number");
|
||||
return res.data as { success: boolean; data: string };
|
||||
}
|
||||
|
||||
export async function getReceivingWarehouses() {
|
||||
const res = await apiClient.get("/receiving/warehouses");
|
||||
return res.data as { success: boolean; data: WarehouseOption[] };
|
||||
}
|
||||
|
||||
// 소스 데이터 조회
|
||||
export async function getPurchaseOrderSources(keyword?: string) {
|
||||
const res = await apiClient.get("/receiving/source/purchase-orders", {
|
||||
params: keyword ? { keyword } : {},
|
||||
});
|
||||
return res.data as { success: boolean; data: PurchaseOrderSource[] };
|
||||
}
|
||||
|
||||
export async function getShipmentSources(keyword?: string) {
|
||||
const res = await apiClient.get("/receiving/source/shipments", {
|
||||
params: keyword ? { keyword } : {},
|
||||
});
|
||||
return res.data as { success: boolean; data: ShipmentSource[] };
|
||||
}
|
||||
|
||||
export async function getItemSources(keyword?: string) {
|
||||
const res = await apiClient.get("/receiving/source/items", {
|
||||
params: keyword ? { keyword } : {},
|
||||
});
|
||||
return res.data as { success: boolean; data: ItemSource[] };
|
||||
}
|
||||
Loading…
Reference in New Issue