Enhance approval process by adding after approval flow ID to templates and implementing user selection via Combobox in the Approval Request Modal.
This commit is contained in:
@@ -2,6 +2,7 @@ import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import { PoolClient } from "pg";
|
||||
import { NodeFlowExecutionService } from "../services/nodeFlowExecutionService";
|
||||
|
||||
// 트랜잭션 내부에서 throw하고 외부에서 instanceof로 구분하기 위한 커스텀 에러
|
||||
class ValidationError extends Error {
|
||||
@@ -354,7 +355,7 @@ export class ApprovalTemplateController {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { template_name, description, definition_id, is_active = "Y", steps = [] } = req.body;
|
||||
const { template_name, description, definition_id, after_approval_flow_id, is_active = "Y", steps = [] } = req.body;
|
||||
|
||||
if (!template_name) {
|
||||
return res.status(400).json({ success: false, message: "템플릿명은 필수입니다." });
|
||||
@@ -365,9 +366,9 @@ export class ApprovalTemplateController {
|
||||
let result: any;
|
||||
await transaction(async (client) => {
|
||||
const { rows } = await client.query(
|
||||
`INSERT INTO approval_line_templates (template_name, description, definition_id, is_active, company_code, created_by, updated_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING *`,
|
||||
[template_name, description, definition_id, is_active, companyCode, userId]
|
||||
`INSERT INTO approval_line_templates (template_name, description, definition_id, after_approval_flow_id, is_active, company_code, created_by, updated_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $7) RETURNING *`,
|
||||
[template_name, description, definition_id, after_approval_flow_id || null, is_active, companyCode, userId]
|
||||
);
|
||||
result = rows[0];
|
||||
|
||||
@@ -422,7 +423,7 @@ export class ApprovalTemplateController {
|
||||
return res.status(404).json({ success: false, message: "결재선 템플릿을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
const { template_name, description, definition_id, is_active, steps } = req.body;
|
||||
const { template_name, description, definition_id, after_approval_flow_id, is_active, steps } = req.body;
|
||||
const userId = req.user?.userId || "system";
|
||||
|
||||
let result: any;
|
||||
@@ -434,6 +435,7 @@ export class ApprovalTemplateController {
|
||||
if (template_name !== undefined) { fields.push(`template_name = $${idx++}`); params.push(template_name); }
|
||||
if (description !== undefined) { fields.push(`description = $${idx++}`); params.push(description); }
|
||||
if (definition_id !== undefined) { fields.push(`definition_id = $${idx++}`); params.push(definition_id); }
|
||||
if (after_approval_flow_id !== undefined) { fields.push(`after_approval_flow_id = $${idx++}`); params.push(after_approval_flow_id); }
|
||||
if (is_active !== undefined) { fields.push(`is_active = $${idx++}`); params.push(is_active); }
|
||||
fields.push(`updated_by = $${idx++}`, `updated_at = NOW()`);
|
||||
params.push(userId);
|
||||
@@ -519,6 +521,131 @@ export class ApprovalTemplateController {
|
||||
// notification step은 자동 통과 후 재귀적으로 다음 step 진행
|
||||
// ============================================================
|
||||
|
||||
// 결재 상태 변경 시 원본 테이블(target_table)의 approval_status를 동기화하는 범용 hook
|
||||
async function syncApprovalStatusToTarget(
|
||||
client: PoolClient,
|
||||
requestId: number,
|
||||
newStatus: string,
|
||||
companyCode: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { rows: [req] } = await client.query(
|
||||
`SELECT target_table, target_record_id FROM approval_requests WHERE request_id = $1 AND company_code = $2`,
|
||||
[requestId, companyCode],
|
||||
);
|
||||
if (!req?.target_table || !req?.target_record_id || req.target_record_id === "0") return;
|
||||
|
||||
const { rows: cols } = await client.query(
|
||||
`SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'approval_status'`,
|
||||
[req.target_table],
|
||||
);
|
||||
if (cols.length === 0) return;
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
in_progress: "결재중",
|
||||
approved: "결재완료",
|
||||
rejected: "반려",
|
||||
cancelled: "작성중",
|
||||
draft: "작성중",
|
||||
post_pending: "후결대기",
|
||||
};
|
||||
|
||||
const businessStatus = statusMap[newStatus] || newStatus;
|
||||
const safeTable = req.target_table.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
|
||||
// super admin(company_code='*')은 다른 회사 레코드도 업데이트 가능
|
||||
if (companyCode === "*") {
|
||||
await client.query(
|
||||
`UPDATE "${safeTable}" SET approval_status = $1 WHERE id = $2`,
|
||||
[businessStatus, req.target_record_id],
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`UPDATE "${safeTable}" SET approval_status = $1 WHERE id = $2 AND company_code = $3`,
|
||||
[businessStatus, req.target_record_id, companyCode],
|
||||
);
|
||||
}
|
||||
|
||||
// 결재 완료(approved) 시 제어관리(노드 플로우) 자동 실행
|
||||
if (newStatus === "approved") {
|
||||
await executeAfterApprovalFlow(client, requestId, companyCode, req);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[syncApprovalStatusToTarget] 원본 테이블 상태 동기화 실패:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 결재 완료 후 제어관리(노드 플로우) 실행 hook
|
||||
// 우선순위: 템플릿(template) > 정의(definition) > 요청(request) 직접 지정
|
||||
async function executeAfterApprovalFlow(
|
||||
client: PoolClient,
|
||||
requestId: number,
|
||||
companyCode: string,
|
||||
approvalReq: { target_table: string; target_record_id: string },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { rows: [reqData] } = await client.query(
|
||||
`SELECT r.after_approval_flow_id, r.definition_id, r.template_id, r.title, r.requester_id
|
||||
FROM approval_requests r WHERE r.request_id = $1`,
|
||||
[requestId],
|
||||
);
|
||||
|
||||
let flowId: number | null = null;
|
||||
|
||||
// 1순위: 템플릿에 연결된 제어관리 플로우
|
||||
if (reqData?.template_id) {
|
||||
const { rows: [tmpl] } = await client.query(
|
||||
`SELECT after_approval_flow_id FROM approval_line_templates WHERE template_id = $1`,
|
||||
[reqData.template_id],
|
||||
);
|
||||
flowId = tmpl?.after_approval_flow_id || null;
|
||||
}
|
||||
|
||||
// 2순위: 정의(definition)에 연결된 제어관리 플로우 (fallback)
|
||||
if (!flowId && reqData?.definition_id) {
|
||||
const { rows: [def] } = await client.query(
|
||||
`SELECT after_approval_flow_id FROM approval_definitions WHERE definition_id = $1`,
|
||||
[reqData.definition_id],
|
||||
);
|
||||
flowId = def?.after_approval_flow_id || null;
|
||||
}
|
||||
|
||||
// 3순위: 요청 자체에 직접 지정된 플로우
|
||||
if (!flowId) {
|
||||
flowId = reqData?.after_approval_flow_id || null;
|
||||
}
|
||||
|
||||
if (!flowId) return;
|
||||
|
||||
// 3. 원본 레코드 데이터 조회
|
||||
const safeTable = approvalReq.target_table.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
const { rows: [targetRecord] } = await client.query(
|
||||
`SELECT * FROM "${safeTable}" WHERE id = $1`,
|
||||
[approvalReq.target_record_id],
|
||||
);
|
||||
|
||||
// 4. 노드 플로우 실행
|
||||
console.log(`[제어관리] 결재 완료 후 플로우 #${flowId} 실행 (request_id=${requestId})`);
|
||||
const result = await NodeFlowExecutionService.executeFlow(flowId, {
|
||||
formData: targetRecord || {},
|
||||
approvalInfo: {
|
||||
requestId,
|
||||
title: reqData.title,
|
||||
requesterId: reqData.requester_id,
|
||||
targetTable: approvalReq.target_table,
|
||||
targetRecordId: approvalReq.target_record_id,
|
||||
},
|
||||
companyCode,
|
||||
selectedRows: targetRecord ? [targetRecord] : [],
|
||||
});
|
||||
|
||||
console.log(`[제어관리] 플로우 #${flowId} 실행 결과: ${result.success ? "성공" : "실패"} (${result.executionTime}ms)`);
|
||||
} catch (err) {
|
||||
// 제어관리 실패는 결재 승인 자체에 영향 주지 않음
|
||||
console.error("[executeAfterApprovalFlow] 제어관리 실행 실패:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function activateNextStep(
|
||||
client: PoolClient,
|
||||
requestId: number,
|
||||
@@ -541,6 +668,7 @@ async function activateNextStep(
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment, requestId, companyCode]
|
||||
);
|
||||
await syncApprovalStatusToTarget(client, requestId, "approved", companyCode);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -561,6 +689,7 @@ async function activateNextStep(
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment, requestId, companyCode]
|
||||
);
|
||||
await syncApprovalStatusToTarget(client, requestId, "approved", companyCode);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -745,7 +874,7 @@ export class ApprovalRequestController {
|
||||
}
|
||||
|
||||
const {
|
||||
title, description, definition_id, target_table, target_record_id,
|
||||
title, description, definition_id, template_id, target_table, target_record_id,
|
||||
target_record_data, screen_id, button_component_id,
|
||||
approvers,
|
||||
approval_mode,
|
||||
@@ -786,16 +915,16 @@ export class ApprovalRequestController {
|
||||
await transaction(async (client) => {
|
||||
const { rows: reqRows } = await client.query(
|
||||
`INSERT INTO approval_requests (
|
||||
title, description, definition_id, target_table, target_record_id,
|
||||
title, description, definition_id, template_id, target_table, target_record_id,
|
||||
target_record_data, status, current_step, total_steps, approval_type,
|
||||
requester_id, requester_name, requester_dept,
|
||||
screen_id, button_component_id, company_code,
|
||||
final_approver_id, completed_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 'approved', 1, 1, 'self',
|
||||
$7, $8, $9, $10, $11, $12, $7, NOW())
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, 'approved', 1, 1, 'self',
|
||||
$8, $9, $10, $11, $12, $13, $8, NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
title, description, definition_id, target_table, safeTargetRecordId,
|
||||
title, description, definition_id, template_id || null, target_table, safeTargetRecordId,
|
||||
JSON.stringify(mergedRecordData),
|
||||
userId, userName, deptName,
|
||||
screen_id, button_component_id, companyCode,
|
||||
@@ -811,6 +940,8 @@ export class ApprovalRequestController {
|
||||
) VALUES ($1, 1, $2, $3, $4, $5, '자기결재', 'approved', 'approval', NOW(), $6)`,
|
||||
[result.request_id, userId, userName, req.user?.positionName || null, deptName, companyCode]
|
||||
);
|
||||
|
||||
await syncApprovalStatusToTarget(client, result.request_id, "approved", companyCode);
|
||||
});
|
||||
|
||||
return res.status(201).json({ success: true, data: result, message: "자기결재(전결) 처리되었습니다." });
|
||||
@@ -902,14 +1033,14 @@ export class ApprovalRequestController {
|
||||
await transaction(async (client) => {
|
||||
const { rows: reqRows } = await client.query(
|
||||
`INSERT INTO approval_requests (
|
||||
title, description, definition_id, target_table, target_record_id,
|
||||
title, description, definition_id, template_id, target_table, target_record_id,
|
||||
target_record_data, status, current_step, total_steps, approval_type,
|
||||
requester_id, requester_name, requester_dept,
|
||||
screen_id, button_component_id, company_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, 1, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 1, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
RETURNING *`,
|
||||
[
|
||||
title, description, definition_id, target_table, safeTargetRecordId,
|
||||
title, description, definition_id, template_id || null, target_table, safeTargetRecordId,
|
||||
JSON.stringify(mergedRecordData), initialStatus, totalSteps, storedApprovalType,
|
||||
userId, userName, deptName,
|
||||
screen_id, button_component_id, companyCode,
|
||||
@@ -971,6 +1102,9 @@ export class ApprovalRequestController {
|
||||
[result.request_id, companyCode]
|
||||
);
|
||||
result.status = "in_progress";
|
||||
await syncApprovalStatusToTarget(client, result.request_id, "in_progress", companyCode);
|
||||
} else {
|
||||
await syncApprovalStatusToTarget(client, result.request_id, "post_pending", companyCode);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1012,10 +1146,13 @@ export class ApprovalRequestController {
|
||||
return res.status(400).json({ success: false, message: "이미 처리된 결재 요청은 회수할 수 없습니다." });
|
||||
}
|
||||
|
||||
await query<any>(
|
||||
"UPDATE approval_requests SET status = 'cancelled', updated_at = NOW() WHERE request_id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
await transaction(async (client) => {
|
||||
await client.query(
|
||||
"UPDATE approval_requests SET status = 'cancelled', updated_at = NOW() WHERE request_id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
await syncApprovalStatusToTarget(client, Number(id), "cancelled", companyCode);
|
||||
});
|
||||
|
||||
return res.json({ success: true, message: "결재 요청이 회수되었습니다." });
|
||||
} catch (error) {
|
||||
@@ -1068,13 +1205,16 @@ export class ApprovalRequestController {
|
||||
return res.status(400).json({ success: false, message: "모든 결재자의 승인이 완료되지 않았습니다." });
|
||||
}
|
||||
|
||||
await query<any>(
|
||||
`UPDATE approval_requests
|
||||
SET status = 'approved', is_post_approved = true, post_approved_at = NOW(),
|
||||
final_approver_id = $1, final_comment = $2, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment || null, id, companyCode]
|
||||
);
|
||||
await transaction(async (client) => {
|
||||
await client.query(
|
||||
`UPDATE approval_requests
|
||||
SET status = 'approved', is_post_approved = true, post_approved_at = NOW(),
|
||||
final_approver_id = $1, final_comment = $2, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment || null, id, companyCode]
|
||||
);
|
||||
await syncApprovalStatusToTarget(client, Number(id), "approved", companyCode);
|
||||
});
|
||||
|
||||
return res.json({ success: true, message: "후결 처리가 완료되었습니다." });
|
||||
} catch (error) {
|
||||
@@ -1110,11 +1250,13 @@ export class ApprovalLineController {
|
||||
}
|
||||
|
||||
await transaction(async (client) => {
|
||||
// FOR UPDATE로 결재 라인 잠금 (동시성 방어)
|
||||
const { rows: [line] } = await client.query(
|
||||
`SELECT * FROM approval_lines WHERE line_id = $1 AND company_code = $2 FOR UPDATE`,
|
||||
[lineId, companyCode]
|
||||
);
|
||||
// FOR UPDATE로 결재 라인 잠금
|
||||
// super admin(*)은 모든 회사의 라인을 처리할 수 있음
|
||||
const lineQuery = companyCode === "*"
|
||||
? `SELECT * FROM approval_lines WHERE line_id = $1 FOR UPDATE`
|
||||
: `SELECT * FROM approval_lines WHERE line_id = $1 AND company_code IN ($2, '*') FOR UPDATE`;
|
||||
const lineParams = companyCode === "*" ? [lineId] : [lineId, companyCode];
|
||||
const { rows: [line] } = await client.query(lineQuery, lineParams);
|
||||
|
||||
if (!line) {
|
||||
throw new ValidationError(404, "결재 라인을 찾을 수 없습니다.");
|
||||
@@ -1129,51 +1271,60 @@ export class ApprovalLineController {
|
||||
let proxyReasonVal: string | null = null;
|
||||
|
||||
if (line.approver_id !== userId) {
|
||||
const { rows: proxyRows } = await client.query(
|
||||
`SELECT * FROM approval_proxy_settings
|
||||
WHERE original_user_id = $1 AND proxy_user_id = $2
|
||||
AND is_active = 'Y' AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE
|
||||
AND company_code = $3`,
|
||||
[line.approver_id, userId, companyCode]
|
||||
);
|
||||
if (proxyRows.length === 0) {
|
||||
throw new ValidationError(403, "본인이 결재자로 지정된 건만 처리할 수 있습니다.");
|
||||
// super admin(company_code='*')은 모든 결재를 대리 처리 가능
|
||||
if (companyCode === "*") {
|
||||
proxyFor = line.approver_id;
|
||||
proxyReasonVal = proxy_reason || "최고관리자 대리 처리";
|
||||
} else {
|
||||
const { rows: proxyRows } = await client.query(
|
||||
`SELECT * FROM approval_proxy_settings
|
||||
WHERE original_user_id = $1 AND proxy_user_id = $2
|
||||
AND is_active = 'Y' AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE
|
||||
AND company_code = $3`,
|
||||
[line.approver_id, userId, companyCode]
|
||||
);
|
||||
if (proxyRows.length === 0) {
|
||||
throw new ValidationError(403, "본인이 결재자로 지정된 건만 처리할 수 있습니다.");
|
||||
}
|
||||
proxyFor = line.approver_id;
|
||||
proxyReasonVal = proxy_reason || proxyRows[0].reason || "대결 처리";
|
||||
}
|
||||
proxyFor = line.approver_id;
|
||||
proxyReasonVal = proxy_reason || proxyRows[0].reason || "대결 처리";
|
||||
}
|
||||
|
||||
// 현재 라인 처리 (proxy_for, proxy_reason 포함)
|
||||
// 현재 라인 처리 (proxy_for, proxy_reason 포함) - 라인의 company_code 기준
|
||||
await client.query(
|
||||
`UPDATE approval_lines
|
||||
SET status = $1, comment = $2, processed_at = NOW(),
|
||||
proxy_for = $3, proxy_reason = $4
|
||||
WHERE line_id = $5 AND company_code = $6`,
|
||||
[action, comment || null, proxyFor, proxyReasonVal, lineId, companyCode]
|
||||
[action, comment || null, proxyFor, proxyReasonVal, lineId, line.company_code]
|
||||
);
|
||||
|
||||
// 결재 요청 조회 (FOR UPDATE)
|
||||
// 결재 요청 조회 (FOR UPDATE) - 라인의 company_code 기준
|
||||
const { rows: [request] } = await client.query(
|
||||
`SELECT * FROM approval_requests WHERE request_id = $1 AND company_code = $2 FOR UPDATE`,
|
||||
[line.request_id, companyCode]
|
||||
[line.request_id, line.company_code]
|
||||
);
|
||||
|
||||
if (!request) return;
|
||||
|
||||
const lineCC = line.company_code;
|
||||
|
||||
if (action === "rejected") {
|
||||
// 반려: 전체 요청 반려 처리
|
||||
await client.query(
|
||||
`UPDATE approval_requests SET status = 'rejected', final_approver_id = $1, final_comment = $2,
|
||||
completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment || null, line.request_id, companyCode]
|
||||
[userId, comment || null, line.request_id, lineCC]
|
||||
);
|
||||
// 남은 pending/waiting 라인도 skipped 처리
|
||||
await client.query(
|
||||
`UPDATE approval_lines SET status = 'skipped'
|
||||
WHERE request_id = $1 AND status IN ('pending', 'waiting') AND line_id != $2 AND company_code = $3`,
|
||||
[line.request_id, lineId, companyCode]
|
||||
[line.request_id, lineId, lineCC]
|
||||
);
|
||||
await syncApprovalStatusToTarget(client, line.request_id, "rejected", lineCC);
|
||||
} else {
|
||||
// 승인 처리: step_type 기반 분기
|
||||
const currentStepType = line.step_type || "approval";
|
||||
@@ -1186,9 +1337,8 @@ export class ApprovalLineController {
|
||||
// 레거시 동시결재 (하위호환)
|
||||
const { rows: remainingLines } = await client.query(
|
||||
`SELECT COUNT(*) as cnt FROM approval_lines
|
||||
WHERE request_id = $1 AND status = 'pending' AND line_id != $2 AND company_code = $3
|
||||
FOR UPDATE`,
|
||||
[line.request_id, lineId, companyCode]
|
||||
WHERE request_id = $1 AND status = 'pending' AND line_id != $2 AND company_code = $3`,
|
||||
[line.request_id, lineId, lineCC]
|
||||
);
|
||||
const remaining = parseInt(remainingLines[0]?.cnt || "0");
|
||||
|
||||
@@ -1197,8 +1347,9 @@ export class ApprovalLineController {
|
||||
`UPDATE approval_requests SET status = 'approved', final_approver_id = $1, final_comment = $2,
|
||||
completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment || null, line.request_id, companyCode]
|
||||
[userId, comment || null, line.request_id, lineCC]
|
||||
);
|
||||
await syncApprovalStatusToTarget(client, line.request_id, "approved", lineCC);
|
||||
}
|
||||
} else if (currentStepType === "consensus") {
|
||||
// 합의결재: 같은 step의 모든 결재자 승인 확인
|
||||
@@ -1206,23 +1357,22 @@ export class ApprovalLineController {
|
||||
`SELECT COUNT(*) as cnt FROM approval_lines
|
||||
WHERE request_id = $1 AND step_order = $2
|
||||
AND status NOT IN ('approved', 'skipped')
|
||||
AND line_id != $3 AND company_code = $4
|
||||
FOR UPDATE`,
|
||||
[line.request_id, line.step_order, lineId, companyCode]
|
||||
AND line_id != $3 AND company_code = $4`,
|
||||
[line.request_id, line.step_order, lineId, lineCC]
|
||||
);
|
||||
|
||||
if (parseInt(remaining[0].cnt) === 0) {
|
||||
// 합의 완료 → 다음 step 활성화
|
||||
await activateNextStep(
|
||||
client, line.request_id, line.step_order, request.total_steps,
|
||||
companyCode, userId, comment || null,
|
||||
lineCC, userId, comment || null,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// approval (기존 sequential 로직): 다음 step 활성화
|
||||
await activateNextStep(
|
||||
client, line.request_id, line.step_order, request.total_steps,
|
||||
companyCode, userId, comment || null,
|
||||
lineCC, userId, comment || null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1260,7 +1410,7 @@ export class ApprovalLineController {
|
||||
`SELECT l.*, r.title, r.target_table, r.target_record_id, r.requester_name, r.requester_dept, r.created_at as request_created_at
|
||||
FROM approval_lines l
|
||||
JOIN approval_requests r ON l.request_id = r.request_id AND l.company_code = r.company_code
|
||||
WHERE l.approver_id = $1 AND l.status = 'pending' AND l.company_code = $2
|
||||
WHERE l.approver_id = $1 AND l.status = 'pending' AND l.company_code IN ($2, '*')
|
||||
ORDER BY r.created_at ASC`,
|
||||
[userId, companyCode]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user