summaryrefslogtreecommitdiff
path: root/db/schema/evaluationTarget.ts
diff options
context:
space:
mode:
Diffstat (limited to 'db/schema/evaluationTarget.ts')
-rw-r--r--db/schema/evaluationTarget.ts319
1 files changed, 319 insertions, 0 deletions
diff --git a/db/schema/evaluationTarget.ts b/db/schema/evaluationTarget.ts
index 343efc4e..c8011e96 100644
--- a/db/schema/evaluationTarget.ts
+++ b/db/schema/evaluationTarget.ts
@@ -844,6 +844,25 @@ export const periodicEvaluationsView = pgView('periodic_evaluations_view').as((q
LIMIT 1
)`.as('cs_eval_status'),
+ // admin 평가 담당 상태
+ adminEvalStatus: sql<string>`(
+ SELECT CASE
+ WHEN re.id IS NULL THEN 'NOT_ASSIGNED'
+ WHEN re.is_completed = true THEN 'COMPLETED'
+ WHEN (
+ SELECT COALESCE(SUM(CAST(red.score AS DECIMAL(5,2))), 0)
+ FROM ${reviewerEvaluationDetails} red
+ WHERE red.reviewer_evaluation_id = re.id
+ ) > 0 THEN 'IN_PROGRESS'
+ ELSE 'NOT_STARTED'
+ END
+ FROM ${reviewerEvaluations} re
+ JOIN ${evaluationTargetReviewers} etr ON re.evaluation_target_reviewer_id = etr.id
+ WHERE re.periodic_evaluation_id = ${periodicEvaluations.id}
+ AND etr.department_code = 'admin'
+ LIMIT 1
+ )`.as('admin_eval_status'),
+
// ═══════════════════════════════════════════════════════════════
// 리뷰어 통계 (서브쿼리로 계산)
// ═══════════════════════════════════════════════════════════════
@@ -1072,3 +1091,303 @@ export interface DepartmentEvaluationStatus {
export type EvaluationProgress = 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED';
export type PeriodicEvaluationStatus = 'PENDING_SUBMISSION' | 'SUBMITTED' | 'IN_REVIEW' | 'REVIEW_COMPLETED' | 'FINALIZED';
+
+
+/**
+ * 배열에서 첫 번째 값을 뽑아내는 헬퍼. 정렬 기준을 주면 해당 기준으로 정렬 후 첫 번째 값을 가져온다.
+ */
+const first = <T>(expr: SQL<T>, order?: SQL) =>
+ order
+ ? sql<T>`(ARRAY_AGG(${expr} ORDER BY ${order} NULLS LAST))[1]`
+ : sql<T>`(ARRAY_AGG(${expr}))[1]`;
+
+/**
+ * BOOL_AND / BOOL_OR 사용이 애매한 경우를 대비한 캐스팅 헬퍼 (선택적으로 사용)
+ * ex) boolMax(expr) → (MAX((expr)::int))::bool
+ */
+const boolMax = (expr: SQL<boolean>) => sql<boolean>`(MAX((${expr})::int))::bool`;
+
+export const periodicEvaluationsAggregatedView = pgView(
+ "periodic_evaluations_aggregated_view"
+).as((qb) =>
+ qb
+ .select({
+ // ═══════════════════════════════════════════════════════════════
+ // 집계된 식별자 및 기본 정보
+ // ═══════════════════════════════════════════════════════════════
+ id: sql<string>`CONCAT(${periodicEvaluationsView.evaluationYear}, '_', ${periodicEvaluationsView.vendorId})`.as(
+ "id"
+ ),
+
+ // 그룹핑 기준 필드들
+ evaluationYear: periodicEvaluationsView.evaluationYear,
+ vendorId: periodicEvaluationsView.vendorId,
+ vendorCode: periodicEvaluationsView.vendorCode,
+ vendorName: periodicEvaluationsView.vendorName,
+ domesticForeign: periodicEvaluationsView.domesticForeign,
+ materialType: periodicEvaluationsView.materialType,
+
+ // ═══════════════════════════════════════════════════════════════
+ // 평가 점수들의 평균 계산
+ // ═══════════════════════════════════════════════════════════════
+ processScore: sql<number>`ROUND(AVG(NULLIF(${periodicEvaluationsView.processScore}, 0)), 1)`.as(
+ "process_score"
+ ),
+ priceScore: sql<number>`ROUND(AVG(NULLIF(${periodicEvaluationsView.priceScore}, 0)), 1)`.as(
+ "price_score"
+ ),
+ deliveryScore: sql<number>`ROUND(AVG(NULLIF(${periodicEvaluationsView.deliveryScore}, 0)), 1)`.as(
+ "delivery_score"
+ ),
+ selfEvaluationScore: sql<number>`ROUND(AVG(NULLIF(${periodicEvaluationsView.selfEvaluationScore}, 0)), 1)`.as(
+ "self_evaluation_score"
+ ),
+ participationBonus: sql<number>`ROUND(AVG(NULLIF(${periodicEvaluationsView.participationBonus}, 0)), 1)`.as(
+ "participation_bonus"
+ ),
+ qualityDeduction: sql<number>`ROUND(AVG(NULLIF(${periodicEvaluationsView.qualityDeduction}, 0)), 1)`.as(
+ "quality_deduction"
+ ),
+
+ // 최종 점수의 평균
+ finalScore: sql<number>`ROUND(AVG(NULLIF(${periodicEvaluationsView.finalScore}, 0)), 1)`.as(
+ "final_score"
+ ),
+
+ // ═══════════════════════════════════════════════════════════════
+ // 평가 등급 (평균 점수를 기반으로 재계산)
+ // ═══════════════════════════════════════════════════════════════
+ evaluationGrade: sql<string>`
+ CASE
+ WHEN (
+ AVG(NULLIF(${periodicEvaluationsView.processScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.priceScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.deliveryScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.selfEvaluationScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.participationBonus}, 0)) -
+ AVG(NULLIF(${periodicEvaluationsView.qualityDeduction}, 0))
+ ) >= 90 THEN 'S'
+ WHEN (
+ AVG(NULLIF(${periodicEvaluationsView.processScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.priceScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.deliveryScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.selfEvaluationScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.participationBonus}, 0)) -
+ AVG(NULLIF(${periodicEvaluationsView.qualityDeduction}, 0))
+ ) >= 80 THEN 'A'
+ WHEN (
+ AVG(NULLIF(${periodicEvaluationsView.processScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.priceScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.deliveryScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.selfEvaluationScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.participationBonus}, 0)) -
+ AVG(NULLIF(${periodicEvaluationsView.qualityDeduction}, 0))
+ ) >= 70 THEN 'B'
+ WHEN (
+ AVG(NULLIF(${periodicEvaluationsView.processScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.priceScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.deliveryScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.selfEvaluationScore}, 0)) +
+ AVG(NULLIF(${periodicEvaluationsView.participationBonus}, 0)) -
+ AVG(NULLIF(${periodicEvaluationsView.qualityDeduction}, 0))
+ ) >= 60 THEN 'C'
+ ELSE 'D'
+ END
+ `.as("evaluation_grade"),
+
+ finalGrade: sql<string>`
+ CASE
+ WHEN AVG(NULLIF(${periodicEvaluationsView.finalScore}, 0)) >= 90 THEN 'S'
+ WHEN AVG(NULLIF(${periodicEvaluationsView.finalScore}, 0)) >= 80 THEN 'A'
+ WHEN AVG(NULLIF(${periodicEvaluationsView.finalScore}, 0)) >= 70 THEN 'B'
+ WHEN AVG(NULLIF(${periodicEvaluationsView.finalScore}, 0)) >= 60 THEN 'C'
+ ELSE 'D'
+ END
+ `.as("final_grade"),
+
+ // ═══════════════════════════════════════════════════════════════
+ // 상태 정보 (우선순위 기반으로 결정)
+ // ═══════════════════════════════════════════════════════════════
+ status: sql<string>`
+ CASE
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.status} = 'FINALIZED' THEN 1 END) = COUNT(*) THEN 'FINALIZED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.status} IN ('REVIEW_COMPLETED', 'FINALIZED') THEN 1 END) = COUNT(*) THEN 'REVIEW_COMPLETED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.status} IN ('IN_REVIEW', 'REVIEW_COMPLETED', 'FINALIZED') THEN 1 END) > 0 THEN 'IN_REVIEW'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.status} IN ('SUBMITTED', 'IN_REVIEW', 'REVIEW_COMPLETED', 'FINALIZED') THEN 1 END) > 0 THEN 'SUBMITTED'
+ ELSE 'PENDING_SUBMISSION'
+ END
+ `.as("status"),
+
+ // ═══════════════════════════════════════════════════════════════
+ // 부서별 평가 상태 (최고 진행 상태로 설정)
+ // ═══════════════════════════════════════════════════════════════
+ orderEvalStatus: sql<string>`
+ CASE
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.orderEvalStatus} = 'COMPLETED' THEN 1 END) > 0 THEN 'COMPLETED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.orderEvalStatus} = 'IN_PROGRESS' THEN 1 END) > 0 THEN 'IN_PROGRESS'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.orderEvalStatus} = 'NOT_STARTED' THEN 1 END) > 0 THEN 'NOT_STARTED'
+ ELSE 'NOT_ASSIGNED'
+ END
+ `.as("order_eval_status"),
+
+ procurementEvalStatus: sql<string>`
+ CASE
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.procurementEvalStatus} = 'COMPLETED' THEN 1 END) > 0 THEN 'COMPLETED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.procurementEvalStatus} = 'IN_PROGRESS' THEN 1 END) > 0 THEN 'IN_PROGRESS'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.procurementEvalStatus} = 'NOT_STARTED' THEN 1 END) > 0 THEN 'NOT_STARTED'
+ ELSE 'NOT_ASSIGNED'
+ END
+ `.as("procurement_eval_status"),
+
+ qualityEvalStatus: sql<string>`
+ CASE
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.qualityEvalStatus} = 'COMPLETED' THEN 1 END) > 0 THEN 'COMPLETED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.qualityEvalStatus} = 'IN_PROGRESS' THEN 1 END) > 0 THEN 'IN_PROGRESS'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.qualityEvalStatus} = 'NOT_STARTED' THEN 1 END) > 0 THEN 'NOT_STARTED'
+ ELSE 'NOT_ASSIGNED'
+ END
+ `.as("quality_eval_status"),
+
+ designEvalStatus: sql<string>`
+ CASE
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.designEvalStatus} = 'COMPLETED' THEN 1 END) > 0 THEN 'COMPLETED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.designEvalStatus} = 'IN_PROGRESS' THEN 1 END) > 0 THEN 'IN_PROGRESS'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.designEvalStatus} = 'NOT_STARTED' THEN 1 END) > 0 THEN 'NOT_STARTED'
+ ELSE 'NOT_ASSIGNED'
+ END
+ `.as("design_eval_status"),
+
+ csEvalStatus: sql<string>`
+ CASE
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.csEvalStatus} = 'COMPLETED' THEN 1 END) > 0 THEN 'COMPLETED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.csEvalStatus} = 'IN_PROGRESS' THEN 1 END) > 0 THEN 'IN_PROGRESS'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.csEvalStatus} = 'NOT_STARTED' THEN 1 END) > 0 THEN 'NOT_STARTED'
+ ELSE 'NOT_ASSIGNED'
+ END
+ `.as("cs_eval_status"),
+
+ adminEvalStatus: sql<string>`
+ CASE
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.adminEvalStatus} = 'COMPLETED' THEN 1 END) > 0 THEN 'COMPLETED'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.adminEvalStatus} = 'IN_PROGRESS' THEN 1 END) > 0 THEN 'IN_PROGRESS'
+ WHEN COUNT(CASE WHEN ${periodicEvaluationsView.adminEvalStatus} = 'NOT_STARTED' THEN 1 END) > 0 THEN 'NOT_STARTED'
+ ELSE 'NOT_ASSIGNED'
+ END
+ `.as("admin_eval_status"),
+
+ // ═══════════════════════════════════════════════════════════════
+ // 문서 제출 및 날짜 정보
+ // ═══════════════════════════════════════════════════════════════
+ documentsSubmitted: sql<boolean>`
+ BOOL_AND(${periodicEvaluationsView.documentsSubmitted})
+ `.as("documents_submitted"),
+
+ submissionDate: sql<Date>`MAX(${periodicEvaluationsView.submissionDate})`.as(
+ "submission_date"
+ ),
+ submissionDeadline: sql<Date>`MAX(${periodicEvaluationsView.submissionDeadline})`.as(
+ "submission_deadline"
+ ),
+ reviewCompletedAt: sql<Date>`MAX(${periodicEvaluationsView.reviewCompletedAt})`.as(
+ "review_completed_at"
+ ),
+ finalizedAt: sql<Date>`MAX(${periodicEvaluationsView.finalizedAt})`.as(
+ "finalized_at"
+ ),
+
+ // ═══════════════════════════════════════════════════════════════
+ // Division 정보 및 집계 메타데이터
+ // ═══════════════════════════════════════════════════════════════
+ division: sql<string>`
+ CASE
+ WHEN COUNT(DISTINCT ${periodicEvaluationsView.division}) > 1 THEN 'BOTH'
+ ELSE MAX(${periodicEvaluationsView.division})
+ END
+ `.as("division"),
+
+ evaluationCount: sql<number>`COUNT(*)::int`.as("evaluation_count"),
+ divisions: sql<string>`STRING_AGG(DISTINCT ${periodicEvaluationsView.division}, ',')`.as(
+ "divisions"
+ ),
+
+ // ═══════════════════════════════════════════════════════════════
+ // 리뷰어 통계 (합계)
+ // ═══════════════════════════════════════════════════════════════
+ totalReviewers: sql<number>`SUM(${periodicEvaluationsView.totalReviewers})::int`.as(
+ "total_reviewers"
+ ),
+ completedReviewers: sql<number>`SUM(${periodicEvaluationsView.completedReviewers})::int`.as(
+ "completed_reviewers"
+ ),
+ pendingReviewers: sql<number>`SUM(${periodicEvaluationsView.pendingReviewers})::int`.as(
+ "pending_reviewers"
+ ),
+
+ // ═══════════════════════════════════════════════════════════════
+ // 기타 정보
+ // ═══════════════════════════════════════════════════════════════
+ evaluationPeriod: sql<string>`MAX(${periodicEvaluationsView.evaluationPeriod})`.as(
+ "evaluation_period"
+ ),
+ evaluationNote: sql<string>`STRING_AGG(${periodicEvaluationsView.evaluationNote}, ' | ')`.as(
+ "evaluation_note"
+ ),
+
+ // 최종 확정자 정보 (가장 최근 확정자)
+ finalizedBy: first<string>(
+ sql`${periodicEvaluationsView.finalizedBy}`,
+ sql`${periodicEvaluationsView.finalizedAt} DESC`
+ ).as("finalized_by"),
+ finalizedByUserName: first<string>(
+ sql`${periodicEvaluationsView.finalizedByUserName}`,
+ sql`${periodicEvaluationsView.finalizedAt} DESC`
+ ).as("finalized_by_user_name"),
+ finalizedByUserEmail: first<string>(
+ sql`${periodicEvaluationsView.finalizedByUserEmail}`,
+ sql`${periodicEvaluationsView.finalizedAt} DESC`
+ ).as("finalized_by_user_email"),
+
+ // 생성/수정 일시
+ createdAt: sql<Date>`MIN(${periodicEvaluationsView.createdAt})`.as(
+ "created_at"
+ ),
+ updatedAt: sql<Date>`MAX(${periodicEvaluationsView.updatedAt})`.as(
+ "updated_at"
+ ),
+
+ // 추가 평가 대상 정보들 (첫 번째 레코드 사용)
+ evaluationTargetId: first<string>(
+ sql`${periodicEvaluationsView.evaluationTargetId}`
+ ).as("evaluation_target_id"),
+
+ evaluationTargetAdminComment: sql<string>`
+ STRING_AGG(DISTINCT ${periodicEvaluationsView.evaluationTargetAdminComment}, ' | ')
+ `.as("evaluation_target_admin_comment"),
+
+ evaluationTargetConsolidatedComment: sql<string>`
+ STRING_AGG(DISTINCT ${periodicEvaluationsView.evaluationTargetConsolidatedComment}, ' | ')
+ `.as("evaluation_target_consolidated_comment"),
+
+ // Boolean / Enum 필드라면 first()로 첫 번째 값만 가져오도록 변경
+ evaluationTargetConsensusStatus: first<string>(
+ sql`${periodicEvaluationsView.evaluationTargetConsensusStatus}`,
+ sql`${periodicEvaluationsView.updatedAt} DESC`
+ ).as("evaluation_target_consensus_status"),
+
+ evaluationTargetConfirmedAt: sql<Date>`
+ MAX(${periodicEvaluationsView.evaluationTargetConfirmedAt})
+ `.as("evaluation_target_confirmed_at"),
+ })
+ .from(periodicEvaluationsView)
+ .groupBy(
+ periodicEvaluationsView.evaluationYear,
+ periodicEvaluationsView.vendorId,
+ periodicEvaluationsView.vendorCode,
+ periodicEvaluationsView.vendorName,
+ periodicEvaluationsView.domesticForeign,
+ periodicEvaluationsView.materialType
+ )
+);
+
+// 타입 정의
+export type PeriodicEvaluationAggregatedView = typeof periodicEvaluationsAggregatedView.$inferSelect; \ No newline at end of file