import { pgTable, serial, integer, varchar, text, timestamp, uniqueIndex, pgView } from "drizzle-orm/pg-core"; import { vendorTypes, vendors } from "./vendors"; import { projects } from "./projects"; import { sql, eq } from "drizzle-orm"; import { users } from "./users"; export const pqCriterias = pgTable("pq_criterias", { id: serial("id").primaryKey(), code: varchar("code", { length: 50 }).notNull(), // 예: "1-1" checkPoint: varchar("check_point", { length: 255 }).notNull(), description: text("description"), remarks: text("remarks"), // (선택) "GENERAL", "Quality Management System" 등 큰 분류 groupName: varchar("group_name", { length: 255 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const pqCriteriasExtension = pgTable("pq_criterias_extension", { id: serial("id").primaryKey(), // pq_criterias와 연결 (1:1 or 1:N 중 필요에 맞춰 사용) pqCriteriaId: integer("pq_criteria_id") .notNull() .references(() => pqCriterias.id, { onDelete: "cascade", onUpdate: "cascade", }), // projects 테이블에 대한 FK projectId: integer("project_id") .notNull() .references(() => projects.id, { onDelete: "cascade", onUpdate: "cascade", }), // 프로젝트별 PQ 시 필요한 추가 정보 contractInfo: text("contract_info"), additionalRequirement: text("additional_requirement"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const vendorCriteriaAttachments = pgTable("vendor_criteria_attachments", { id: serial("id").primaryKey(), vendorCriteriaAnswerId: integer("vendor_criteria_answer_id") .references(() => vendorPqCriteriaAnswers.id, { onDelete: "cascade" }) .notNull(), fileName: varchar("file_name", { length: 255 }).notNull(), filePath: varchar("file_path", { length: 1024 }).notNull(), fileType: varchar("file_type", { length: 50 }), fileSize: integer("file_size"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const vendorPqReviewLogs = pgTable("vendor_pq_review_logs", { id: serial("id").primaryKey(), // Each log references a single vendorPqCriteriaAnswers row vendorPqCriteriaAnswerId: integer("vendor_pq_criteria_answer_id") .references(() => vendorPqCriteriaAnswers.id, { onDelete: "cascade" }) .notNull(), // The reviewer's comment text reviewerComment: text("reviewer_comment").notNull(), // Optionally store the reviewer name or user ID, if you have it reviewerName: text("reviewer_name"), createdAt: timestamp("created_at").defaultNow().notNull(), }) export type PqCriterias = typeof pqCriterias.$inferSelect // 협력업체와 프로젝트 PQ 요청 연결 테이블 export const vendorProjectPQs = pgTable("vendor_project_pqs", { id: serial("id").primaryKey(), // vendors 테이블 FK vendorId: integer("vendor_id") .notNull() .references(() => vendors.id, { onDelete: "cascade", onUpdate: "cascade", }), // projects 테이블 FK projectId: integer("project_id") .notNull() .references(() => projects.id, { onDelete: "cascade", onUpdate: "cascade", }), // 상태: REQUESTED(요청됨), IN_PROGRESS(진행중), SUBMITTED(제출됨), APPROVED(승인됨), REJECTED(거부됨) status: varchar("status", { length: 20 }).notNull().default("REQUESTED"), // 메타데이터 submittedAt: timestamp("submitted_at"), approvedAt: timestamp("approved_at"), rejectedAt: timestamp("rejected_at"), rejectReason: text("reject_reason"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const vendorPQSubmissions = pgTable("vendor_pq_submissions", { id: serial("id").primaryKey(), pqNumber: varchar("pq_number", { length: 50 }).notNull().unique(), requesterId: integer("requester_id") .references(() => users.id), vendorId: integer("vendor_id") .notNull() .references(() => vendors.id, { onDelete: "cascade", onUpdate: "cascade", }), // null이면 일반 PQ, 값이 있으면 프로젝트 PQ projectId: integer("project_id") .references(() => projects.id, { onDelete: "cascade", onUpdate: "cascade", }), // PQ 유형 구분을 명시적으로 type: varchar("type", { length: 20 }).notNull(), // "GENERAL" or "PROJECT" status: varchar("status", { length: 20 }).notNull().default("REQUESTED"), submittedAt: timestamp("submitted_at"), approvedAt: timestamp("approved_at"), rejectedAt: timestamp("rejected_at"), rejectReason: text("reject_reason"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }, (table) => { return { // 협력업체별로 일반 PQ는 하나만, 프로젝트 PQ는 프로젝트당 하나만 uniqueConstraint: uniqueIndex("unique_pq_submission").on( table.vendorId, table.projectId, table.type ), }; }); // 기존 vendorPqCriteriaAnswers 테이블에 projectId 필드 추가 export const vendorPqCriteriaAnswers = pgTable("vendor_pq_criteria_answers", { id: serial("id").primaryKey(), vendorId: integer("vendor_id") .notNull() .references(() => vendors.id, { onDelete: "cascade", onUpdate: "cascade", }), criteriaId: integer("criteria_id") .notNull() .references(() => pqCriterias.id, { onDelete: "cascade", onUpdate: "cascade", }), // 추가: 프로젝트 ID (null은 일반 PQ를 의미) projectId: integer("project_id") .references(() => projects.id, { onDelete: "cascade", onUpdate: "cascade", }), answer: text("answer"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const projectApprovedVendors = pgView("project_approved_vendors").as((qb) => { return qb .select({ vendor_id: vendors.id, vendor_name: vendors.vendorName, vendor_code: vendors.vendorCode, tax_id: vendors.taxId, vendor_email: vendors.email, vendor_phone: vendors.phone, vendor_status: vendors.status, // vendor_type_code: vendorTypes.code, vendor_type_name_ko: vendorTypes.nameKo, vendor_type_name_en: vendorTypes.nameEn, project_code: projects.code, project_name: projects.name, project_type: projects.type, // pq_status: vendorProjectPQs.status, submitted_at: vendorPQSubmissions.submittedAt, approved_at: vendorPQSubmissions.approvedAt }) .from(vendors) .innerJoin( vendorPQSubmissions, sql`${vendorPQSubmissions.vendorId} = ${vendors.id}` ) .innerJoin( projects, sql`${vendorPQSubmissions.projectId} = ${projects.id}` ) .leftJoin( vendorTypes, sql`${vendors.vendorTypeId} = ${vendorTypes.id}` ) .where(sql`${vendorPQSubmissions.status} = 'APPROVED'`); }); export type ProjectApprovedVendors = typeof projectApprovedVendors.$inferSelect export const vendorInvestigations = pgTable("vendor_investigations", { id: serial("id").primaryKey(), // 어떤 벤더에 대한 실사인지 참조 vendorId: integer("vendor_id").notNull().references(() => vendors.id), // PQ 제출에 대한 참조 pqSubmissionId: integer("pq_submission_id") .references(() => vendorPQSubmissions.id, { onDelete: "set null", onUpdate: "cascade", }), requesterId: integer("requester_id") .references(() => users.id), qmManagerId: integer("qm_manager_id") .references(() => users.id), // 실사 상태 investigationStatus: varchar("investigation_status", { length: 50, enum: [ "PLANNED", // 계획됨 "IN_PROGRESS", // 진행 중 "COMPLETED", // 완료됨 "CANCELED", // 취소됨 ], }) .notNull() .default("PLANNED"), // 평가 유형 evaluationType: varchar("evaluation_type", { length: 50, enum: [ "SITE_AUDIT", // 실사의뢰평가 "QM_SELF_AUDIT", // QM자체평가 ], }), // 실사 주소 investigationAddress: text("investigation_address"), // 실사 방법 investigationMethod: varchar("investigation_method", { length: 100 }), // 실사 일정 시작일 / 종료일 scheduledStartAt: timestamp("scheduled_start_at"), scheduledEndAt: timestamp("scheduled_end_at"), // 실사 예정일 forecastedAt: timestamp("forecasted_at"), // 실사 의뢰일 requestedAt: timestamp("requested_at"), // 실사 확정일 confirmedAt: timestamp("confirmed_at"), // 실제 실사 완료일 completedAt: timestamp("completed_at"), // 실사 평가점수 evaluationScore: integer("evaluation_score"), // 실사 평가 결과 evaluationResult: varchar("evaluation_result", { length: 50, enum: [ "APPROVED", // 승인 "SUPPLEMENT", // 보완 "REJECTED", // 불가 ], }), // 실사 내용이나 특이사항 investigationNotes: text("investigation_notes"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); // 타입 정의 export type VendorInvestigation = typeof vendorInvestigations.$inferSelect; export const vendorInvestigationAttachments = pgTable( "vendor_investigation_attachments", { id: serial("id").primaryKey(), // 어떤 실사 (investigation)에 대한 첨부파일인지 investigationId: integer("investigation_id") .notNull() .references(() => vendorInvestigations.id, { onDelete: "cascade" }), fileName: varchar("file_name", { length: 255 }).notNull(), filePath: varchar("file_path", { length: 1024 }).notNull(), // 권장: 사용자 경험과 기능성을 위해 추가 fileSize: integer("file_size"), // bytes (nullable로 기존 데이터 호환) mimeType: varchar("mime_type", { length: 100 }), // nullable로 기존 데이터 호환 // 첨부파일 종류 (예: 보고서, 사진, 기타 등 구분) attachmentType: varchar("attachment_type", { length: 50 }).default("REPORT"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), } ); // 타입 정의 export type VendorInvestigationAttachment = typeof vendorInvestigationAttachments.$inferSelect; export type NewVendorInvestigationAttachment = typeof vendorInvestigationAttachments.$inferInsert; // 파일 업로드 관련 유틸리티 타입 export interface FileUploadResult { id: number; fileName: string; originalFileName: string; filePath: string; fileSize: number; mimeType: string; fileType: string; uploadedBy: number; description?: string; } // 파일 다운로드용 타입 export interface FileDownloadInfo { fileName: string; originalFileName: string; filePath: string; mimeType: string; fileSize: number; } /** * A view that joins vendor_investigations + vendors, * and also embeds contacts & possibleItems as JSON arrays. */ export const vendorInvestigationsView = pgView( "vendor_investigations_view" ).as((qb) => { return qb .select({ // Investigation fields - use investigationId as the primary identifier investigationId: vendorInvestigations.id, vendorId: vendorInvestigations.vendorId, pqSubmissionId: vendorInvestigations.pqSubmissionId, requesterId: vendorInvestigations.requesterId, qmManagerId: vendorInvestigations.qmManagerId, investigationStatus: vendorInvestigations.investigationStatus, evaluationType: vendorInvestigations.evaluationType, investigationAddress: vendorInvestigations.investigationAddress, investigationMethod: vendorInvestigations.investigationMethod, scheduledStartAt: vendorInvestigations.scheduledStartAt, scheduledEndAt: vendorInvestigations.scheduledEndAt, forecastedAt: vendorInvestigations.forecastedAt, requestedAt: vendorInvestigations.requestedAt, confirmedAt: vendorInvestigations.confirmedAt, completedAt: vendorInvestigations.completedAt, evaluationScore: vendorInvestigations.evaluationScore, evaluationResult: vendorInvestigations.evaluationResult, investigationNotes: vendorInvestigations.investigationNotes, createdAt: vendorInvestigations.createdAt, updatedAt: vendorInvestigations.updatedAt, // Essential vendor fields vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, // User names and emails instead of just IDs requesterName: sql`requester.name`.as("requesterName"), requesterEmail: sql`requester.email`.as("requesterEmail"), qmManagerName: sql`qm_manager.name`.as("qmManagerName"), qmManagerEmail: sql`qm_manager.email`.as("qmManagerEmail"), // File attachment status hasAttachments: sql`( CASE WHEN EXISTS ( SELECT 1 FROM vendor_investigation_attachments via WHERE via.investigation_id = ${vendorInvestigations.id} ) THEN true ELSE false END )`.as("hasAttachments"), }) .from(vendorInvestigations) .leftJoin( vendors, eq(vendorInvestigations.vendorId, vendors.id) ) .leftJoin( sql`users AS requester`, eq(vendorInvestigations.requesterId, sql`requester.id`) ) .leftJoin( sql`users AS qm_manager`, eq(vendorInvestigations.qmManagerId, sql`qm_manager.id`) ) })