summaryrefslogtreecommitdiff
path: root/lib/forms/vendor-completion-stats.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
commitb67e36df49f067cbd5ba899f9fbcc755f38d4b4f (patch)
tree5a71c5960f90d988cd509e3ef26bff497a277661 /lib/forms/vendor-completion-stats.ts
parentb7f54b06c1ef9e619f5358fb0a5caad9703c8905 (diff)
(대표님, 최겸, 임수민) 작업사항 커밋
Diffstat (limited to 'lib/forms/vendor-completion-stats.ts')
-rw-r--r--lib/forms/vendor-completion-stats.ts340
1 files changed, 296 insertions, 44 deletions
diff --git a/lib/forms/vendor-completion-stats.ts b/lib/forms/vendor-completion-stats.ts
index db2d376d..97efec30 100644
--- a/lib/forms/vendor-completion-stats.ts
+++ b/lib/forms/vendor-completion-stats.ts
@@ -1,8 +1,8 @@
"use server";
import db from "@/db/db";
-import {
- formMetas,
+import {
+ formMetas,
formEntries,
tags,
tagClasses,
@@ -66,6 +66,7 @@ export interface VendorAllContractsCompletionSummary {
contracts: VendorContractCompletionStats[];
totalContracts: number;
totalForms: number;
+ totalTags: number;
totalRequiredFields: number;
totalFilledFields: number;
totalEmptyFields: number;
@@ -82,18 +83,21 @@ export interface VendorAllContractsCompletionSummary {
/**
* 필드가 벤더에 의해 편집 가능한지 확인
- * SHI 값이 "BOTH" 또는 "IN"인 경우만 벤더가 편집 가능
+ * SHI 값이 "BOTH" 또는 "IN"인 경우만 벤더가 편집 가능 (대소문자 무관)
*/
function isFieldEditableByVendor(column: DataTableColumnJSON): boolean {
- return column.shi === "BOTH" || column.shi === "IN";
+ const shi = column.shi?.toString().toUpperCase();
+ const isEditable = shi === "BOTH" || shi === "IN";
+ console.log(`isFieldEditableByVendor - Key: ${column.key}, shi: ${column.shi}, upperShi: ${shi}, isEditable: ${isEditable}`);
+ return isEditable;
}
/**
* 특정 태그에 대해 편집 가능한 필드 목록을 가져옴
*/
async function getEditableFieldsForTag(
- tagNo: string,
- contractItemId: number,
+ tagNo: string,
+ contractItemId: number,
projectId: number
): Promise<string[]> {
try {
@@ -112,8 +116,11 @@ async function getEditableFieldsForTag(
.limit(1);
if (tagResult.length === 0) {
+ console.log(`getEditableFieldsForTag - No tag found for tagNo: ${tagNo}, contractItemId: ${contractItemId}`);
return [];
}
+
+ console.log(`getEditableFieldsForTag - Found tag for tagNo: ${tagNo}, class: ${tagResult[0].tagClass}`);
// 2. tagClasses에서 해당 class와 projectId로 tagClass 찾기
const tagClassResult = await db
@@ -128,8 +135,11 @@ async function getEditableFieldsForTag(
.limit(1);
if (tagClassResult.length === 0) {
+ console.log(`getEditableFieldsForTag - No tag class found for class: ${tagResult[0].tagClass}, projectId: ${projectId}`);
return [];
}
+
+ console.log(`getEditableFieldsForTag - Found tag class: ${tagClassResult[0].id} for class: ${tagResult[0].tagClass}`);
// 3. tagClassAttributes에서 편집 가능한 필드 목록 조회
const editableAttributes = await db
@@ -138,6 +148,8 @@ async function getEditableFieldsForTag(
.where(eq(tagClassAttributes.tagClassId, tagClassResult[0].id))
.orderBy(tagClassAttributes.seq);
+ console.log(`getEditableFieldsForTag - Found ${editableAttributes.length} editable attributes for tag class ${tagClassResult[0].id}:`, editableAttributes.map(attr => attr.attId));
+
return editableAttributes.map(attr => attr.attId);
} catch (error) {
console.error(`Error getting editable fields for tag ${tagNo}:`, error);
@@ -191,12 +203,7 @@ export async function calculateVendorFormCompletion(
const metaRows = await db
.select()
.from(formMetas)
- .where(
- and(
- eq(formMetas.formCode, formCode),
- eq(formMetas.projectId, projectId)
- )
- )
+ .where(eq(formMetas.formCode, formCode))
.orderBy(desc(formMetas.updatedAt))
.limit(1);
@@ -205,6 +212,8 @@ export async function calculateVendorFormCompletion(
console.warn(`No form meta found for formCode: ${formCode} and projectId: ${projectId}`);
return null;
}
+
+ console.log(`calculateVendorFormCompletion - Found form meta for formCode: ${formCode}, projectId: ${projectId}, columns type: ${typeof meta.columns}, isArray: ${Array.isArray(meta.columns)}`);
// 3. Form 실제 데이터 조회
const entryRows = await db
@@ -227,10 +236,15 @@ export async function calculateVendorFormCompletion(
// 4. 컬럼 정의에서 벤더가 편집 가능한 필드 필터링
const columns = meta.columns as DataTableColumnJSON[];
- const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO', 'status', 'TAG_NO', 'TAG_DESC'];
- const editableColumns = columns.filter(col =>
+ const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO', 'status'];
+ const editableColumns = columns.filter(col =>
!excludeKeys.includes(col.key) && isFieldEditableByVendor(col)
);
+
+ console.log(`calculateVendorFormCompletion - Total columns: ${columns.length}, Editable columns: ${editableColumns.length}`);
+ console.log(`calculateVendorFormCompletion - Editable column keys:`, editableColumns.map(col => col.key));
+ console.log(`calculateVendorFormCompletion - All column keys:`, columns.map(col => col.key));
+ console.log(`calculateVendorFormCompletion - All column shi values:`, columns.map(col => col.shi));
// 5. 각 태그별로 완성도 계산
const detailsByTag: VendorFormCompletionStats['detailsByTag'] = [];
@@ -243,13 +257,8 @@ export async function calculateVendorFormCompletion(
const tagNo = rowData.TAG_NO as string;
if (!tagNo) continue;
- // 이 태그에 대해 실제로 편집 가능한 필드 목록 가져오기
- const tagEditableFields = await getEditableFieldsForTag(tagNo, contractItemId, projectId);
-
- // 컬럼 정의와 태그별 편집 가능 필드를 교집합으로 구해서 실제 편집 가능한 필드 확정
- const actualEditableFields = editableColumns.filter(col =>
- tagEditableFields.includes(col.key)
- );
+ // Debug 페이지와 동일하게 직접 editableColumns 사용 (getEditableFieldsForTag 대신)
+ const actualEditableFields = editableColumns;
const requiredFieldsCount = actualEditableFields.length;
let filledFieldsCount = 0;
@@ -263,8 +272,8 @@ export async function calculateVendorFormCompletion(
}
const emptyFieldsCount = requiredFieldsCount - filledFieldsCount;
- const completionPercentage = requiredFieldsCount > 0
- ? Math.round((filledFieldsCount / requiredFieldsCount) * 100)
+ const completionPercentage = requiredFieldsCount > 0
+ ? Math.round((filledFieldsCount / requiredFieldsCount) * 100)
: 100;
detailsByTag.push({
@@ -280,8 +289,8 @@ export async function calculateVendorFormCompletion(
}
const totalEmptyFields = totalRequiredFields - totalFilledFields;
- const overallCompletionPercentage = totalRequiredFields > 0
- ? Math.round((totalFilledFields / totalRequiredFields) * 100)
+ const overallCompletionPercentage = totalRequiredFields > 0
+ ? Math.round((totalFilledFields / totalRequiredFields) * 100)
: 100;
return {
@@ -348,13 +357,13 @@ export async function getProjectVendorCompletionSummary(
// 3. 각 contract item별로 완성도 계산
const vendorStats: VendorFormCompletionStats[] = [];
-
+
for (const item of contractItemsInfo) {
const stats = await calculateVendorFormCompletion(
- item.contractItemId,
+ item.contractItemId,
formCode
);
-
+
if (stats) {
vendorStats.push(stats);
}
@@ -363,8 +372,8 @@ export async function getProjectVendorCompletionSummary(
// 4. 전체 평균 완성도 계산
const averageCompletionPercentage = vendorStats.length > 0
? Math.round(
- vendorStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / vendorStats.length
- )
+ vendorStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / vendorStats.length
+ )
: 0;
return {
@@ -447,8 +456,8 @@ export async function calculateVendorContractCompletion(
const totalEmptyFields = totalRequiredFields - totalFilledFields;
const averageCompletionPercentage = formStats.length > 0
? Math.round(
- formStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / formStats.length
- )
+ formStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / formStats.length
+ )
: 0;
return {
@@ -515,15 +524,23 @@ export async function getVendorAllContractsCompletionSummary(
// 3. 각 contract item별로 완성도 계산
const contractStats: VendorContractCompletionStats[] = [];
-
+
for (const item of contractItemsInfo) {
+ console.log(`getVendorAllContractsCompletionSummary - Processing contract item: ${item.contractItemId} for vendor: ${vendorId}`);
const contractCompletion = await calculateVendorContractCompletion(
- vendorId,
+ vendorId,
item.contractItemId
);
-
+
if (contractCompletion) {
+ console.log(`getVendorAllContractsCompletionSummary - Contract completion for item ${item.contractItemId}:`, {
+ totalRequiredFields: contractCompletion.totalRequiredFields,
+ totalFilledFields: contractCompletion.totalFilledFields,
+ totalForms: contractCompletion.totalForms
+ });
contractStats.push(contractCompletion);
+ } else {
+ console.log(`getVendorAllContractsCompletionSummary - No contract completion for item: ${item.contractItemId}`);
}
}
@@ -532,7 +549,10 @@ export async function getVendorAllContractsCompletionSummary(
const totalFilledFields = contractStats.reduce((sum, stat) => sum + stat.totalFilledFields, 0);
const totalEmptyFields = totalRequiredFields - totalFilledFields;
const totalForms = contractStats.reduce((sum, stat) => sum + stat.totalForms, 0);
-
+ const totalTags = contractStats.reduce((sum, stat) =>
+ sum + stat.forms.reduce((formSum, form) => formSum + form.tagCount, 0), 0
+ );
+
const overallCompletionPercentage = totalRequiredFields > 0
? Math.round((totalFilledFields / totalRequiredFields) * 100)
: 100;
@@ -581,6 +601,7 @@ export async function getVendorAllContractsCompletionSummary(
contracts: contractStats,
totalContracts: contractStats.length,
totalForms,
+ totalTags,
totalRequiredFields,
totalFilledFields,
totalEmptyFields,
@@ -614,7 +635,7 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{
// 2. 각 프로젝트별로 form들의 완성도 조회
const projectSummaries: ProjectVendorCompletionSummary[] = [];
-
+
for (const project of allProjects) {
// 해당 프로젝트의 모든 form codes 조회
const formCodes = await db
@@ -626,7 +647,7 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{
// 각 form에 대한 완성도 조회 후 통합
const allVendorStats: VendorFormCompletionStats[] = [];
-
+
for (const { formCode } of formCodes) {
const summary = await getProjectVendorCompletionSummary(project.id, formCode);
if (summary) {
@@ -653,8 +674,8 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{
// 3. 전체 평균 계산
const overallAverageCompletion = projectSummaries.length > 0
? Math.round(
- projectSummaries.reduce((sum, proj) => sum + proj.averageCompletionPercentage, 0) / projectSummaries.length
- )
+ projectSummaries.reduce((sum, proj) => sum + proj.averageCompletionPercentage, 0) / projectSummaries.length
+ )
: 0;
return {
@@ -674,6 +695,227 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{
}
/**
+ * 특정 벤더의 필드 계산 상세 정보를 디버깅용으로 반환
+ */
+export async function debugVendorFieldCalculation(vendorId: number): Promise<{
+ vendorId: number;
+ vendorName: string;
+ debugInfo: {
+ contracts: Array<{
+ contractId: number;
+ contractItemId: number;
+ projectName: string;
+ forms: Array<{
+ formCode: string;
+ formName: string;
+ tags: Array<{
+ tagNo: string;
+ editableFields: string[];
+ requiredFieldsCount: number;
+ filledFieldsCount: number;
+ fieldDetails: Array<{
+ fieldKey: string;
+ fieldValue: unknown;
+ isEmpty: boolean;
+ }>;
+ }>;
+ totalRequiredFields: number;
+ totalFilledFields: number;
+ }>;
+ totalRequiredFields: number;
+ totalFilledFields: number;
+ }>;
+ grandTotal: {
+ totalRequiredFields: number;
+ totalFilledFields: number;
+ totalEmptyFields: number;
+ completionPercentage: number;
+ };
+ };
+} | null> {
+ try {
+ // 1. 벤더 정보 조회
+ const vendorInfo = await db
+ .select({
+ id: vendors.id,
+ vendorName: vendors.vendorName
+ })
+ .from(vendors)
+ .where(eq(vendors.id, vendorId))
+ .limit(1);
+
+ if (vendorInfo.length === 0) {
+ console.warn(`No vendor found with ID: ${vendorId}`);
+ return null;
+ }
+
+ const vendor = vendorInfo[0];
+
+ // 2. 해당 벤더의 모든 contract items 조회
+ const contractItemsInfo = await db
+ .select({
+ contractId: contracts.id,
+ contractItemId: contractItems.id,
+ projectId: projects.id,
+ projectCode: projects.code,
+ projectName: projects.name,
+ itemId: contractItems.itemId,
+ description: contractItems.description
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
+ .innerJoin(projects, eq(contracts.projectId, projects.id))
+ .where(eq(contracts.vendorId, vendorId));
+
+ const debugContracts = [];
+
+ for (const item of contractItemsInfo) {
+ // 3. 해당 contract item과 연관된 모든 form codes 조회
+ const formCodes = await db
+ .selectDistinct({
+ formCode: formEntries.formCode
+ })
+ .from(formEntries)
+ .where(eq(formEntries.contractItemId, item.contractItemId));
+
+ const debugForms = [];
+ let contractTotalRequired = 0;
+ let contractTotalFilled = 0;
+
+ for (const { formCode } of formCodes) {
+ // 4. Form 메타데이터 조회
+ const metaRows = await db
+ .select()
+ .from(formMetas)
+ .where(eq(formMetas.formCode, formCode))
+ .orderBy(desc(formMetas.updatedAt))
+ .limit(1);
+
+ const meta = metaRows[0];
+ if (!meta) {
+ console.log(`No form meta found for formCode: ${formCode}, projectId: ${item.projectId}`);
+ continue;
+ }
+
+ console.log(`Found form meta for formCode: ${formCode}, projectId: ${item.projectId}, columns type: ${typeof meta.columns}, isArray: ${Array.isArray(meta.columns)}`);
+
+ // 5. Form 실제 데이터 조회
+ const entryRows = await db
+ .select()
+ .from(formEntries)
+ .where(
+ and(
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, item.contractItemId)
+ )
+ )
+ .orderBy(desc(formEntries.updatedAt))
+ .limit(1);
+
+ const entry = entryRows[0];
+ if (!entry || !Array.isArray(entry.data)) continue;
+
+ // 6. 컬럼 정의에서 벤더가 편집 가능한 필드 필터링
+ const columns = meta.columns as DataTableColumnJSON[];
+ const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO', 'status'];
+ const editableColumns = columns.filter(col =>
+ !excludeKeys.includes(col.key) && isFieldEditableByVendor(col)
+ );
+
+ const debugTags = [];
+ let formTotalRequired = 0;
+ let formTotalFilled = 0;
+
+ const formData = entry.data as Array<Record<string, unknown>>;
+
+ for (const rowData of formData) {
+ const tagNo = rowData.TAG_NO as string;
+ if (!tagNo) continue;
+
+ // 직접 editableColumns 사용 (getEditableFieldsForTag 대신)
+ const actualEditableFields = editableColumns;
+
+ const requiredFieldsCount = actualEditableFields.length;
+ let filledFieldsCount = 0;
+
+ const fieldDetails = [];
+ // 각 편집 가능한 필드의 값 확인
+ for (const column of actualEditableFields) {
+ const value = rowData[column.key];
+ const isEmpty = isEmptyValue(value);
+ if (!isEmpty) {
+ filledFieldsCount++;
+ }
+ fieldDetails.push({
+ fieldKey: column.key,
+ fieldValue: value,
+ isEmpty
+ });
+ }
+
+ debugTags.push({
+ tagNo,
+ editableFields: actualEditableFields.map(col => col.key),
+ requiredFieldsCount,
+ filledFieldsCount,
+ fieldDetails
+ });
+
+ formTotalRequired += requiredFieldsCount;
+ formTotalFilled += filledFieldsCount;
+ }
+
+ debugForms.push({
+ formCode,
+ formName: meta.formName,
+ tags: debugTags,
+ totalRequiredFields: formTotalRequired,
+ totalFilledFields: formTotalFilled
+ });
+
+ contractTotalRequired += formTotalRequired;
+ contractTotalFilled += formTotalFilled;
+ }
+
+ debugContracts.push({
+ contractId: item.contractId,
+ contractItemId: item.contractItemId,
+ projectName: item.projectName,
+ forms: debugForms,
+ totalRequiredFields: contractTotalRequired,
+ totalFilledFields: contractTotalFilled
+ });
+ }
+
+ // 전체 합계 계산
+ const grandTotalRequired = debugContracts.reduce((sum, contract) => sum + contract.totalRequiredFields, 0);
+ const grandTotalFilled = debugContracts.reduce((sum, contract) => sum + contract.totalFilledFields, 0);
+ const grandTotalEmpty = grandTotalRequired - grandTotalFilled;
+ const grandCompletionPercentage = grandTotalRequired > 0
+ ? Math.round((grandTotalFilled / grandTotalRequired) * 100)
+ : 100;
+
+ return {
+ vendorId: vendor.id,
+ vendorName: vendor.vendorName,
+ debugInfo: {
+ contracts: debugContracts,
+ grandTotal: {
+ totalRequiredFields: grandTotalRequired,
+ totalFilledFields: grandTotalFilled,
+ totalEmptyFields: grandTotalEmpty,
+ completionPercentage: grandCompletionPercentage
+ }
+ }
+ };
+
+ } catch (error) {
+ console.error(`Error debugging vendor field calculation:`, error);
+ return null;
+ }
+}
+
+/**
* 모든 벤더들의 전체 계약 완성도 요약 (관리자용)
*/
export async function getAllVendorsContractsCompletionSummary(): Promise<{
@@ -704,24 +946,33 @@ export async function getAllVendorsContractsCompletionSummary(): Promise<{
// 2. 각 벤더별로 완성도 계산
const vendorSummaries: VendorAllContractsCompletionSummary[] = [];
-
+
for (const vendor of vendorsWithContracts) {
+ console.log(`getAllVendorsContractsCompletionSummary - Processing vendor: ${vendor.vendorId} (${vendor.vendorName})`);
const summary = await getVendorAllContractsCompletionSummary(vendor.vendorId);
if (summary) {
+ console.log(`getAllVendorsContractsCompletionSummary - Vendor ${vendor.vendorId} summary:`, {
+ totalRequiredFields: summary.totalRequiredFields,
+ totalFilledFields: summary.totalFilledFields,
+ totalTags: summary.totalTags,
+ totalForms: summary.totalForms
+ });
vendorSummaries.push(summary);
+ } else {
+ console.log(`getAllVendorsContractsCompletionSummary - No summary for vendor: ${vendor.vendorId}`);
}
}
// 3. 전체 평균 계산
const overallAverageCompletion = vendorSummaries.length > 0
? Math.round(
- vendorSummaries.reduce((sum, vendor) => sum + vendor.overallCompletionPercentage, 0) / vendorSummaries.length
- )
+ vendorSummaries.reduce((sum, vendor) => sum + vendor.overallCompletionPercentage, 0) / vendorSummaries.length
+ )
: 0;
// 4. 상위/하위 성과 벤더 추출 (상위 5개, 하위 5개)
const sortedVendors = [...vendorSummaries].sort((a, b) => b.overallCompletionPercentage - a.overallCompletionPercentage);
-
+
const topPerformingVendors = sortedVendors.slice(0, 5).map(vendor => ({
vendorId: vendor.vendorId,
vendorName: vendor.vendorName,
@@ -753,3 +1004,4 @@ export async function getAllVendorsContractsCompletionSummary(): Promise<{
};
}
}
+