diff options
Diffstat (limited to 'db/schema')
| -rw-r--r-- | db/schema/companies.ts | 13 | ||||
| -rw-r--r-- | db/schema/contract.ts | 227 | ||||
| -rw-r--r-- | db/schema/items.ts | 12 | ||||
| -rw-r--r-- | db/schema/pq.ts | 67 | ||||
| -rw-r--r-- | db/schema/projects.ts | 13 | ||||
| -rw-r--r-- | db/schema/rfq.ts | 743 | ||||
| -rw-r--r-- | db/schema/tasks.ts | 46 | ||||
| -rw-r--r-- | db/schema/users.ts | 147 | ||||
| -rw-r--r-- | db/schema/vendorData.ts | 205 | ||||
| -rw-r--r-- | db/schema/vendorDocu.ts | 285 | ||||
| -rw-r--r-- | db/schema/vendors.ts | 118 |
11 files changed, 1876 insertions, 0 deletions
diff --git a/db/schema/companies.ts b/db/schema/companies.ts new file mode 100644 index 00000000..60f8a0ce --- /dev/null +++ b/db/schema/companies.ts @@ -0,0 +1,13 @@ +import { pgTable, varchar, integer, timestamp } from "drizzle-orm/pg-core"; + +export const companies = pgTable("companies", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + name: varchar("name", { length: 255 }).notNull(), + taxID: integer("taxID").notNull(), + // 회사가 여러 군데일 경우를 대비하여 unique는 걸지 않을 수도 있음 + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), +}); + +export type Company = typeof companies.$inferSelect diff --git a/db/schema/contract.ts b/db/schema/contract.ts new file mode 100644 index 00000000..3c29f0d0 --- /dev/null +++ b/db/schema/contract.ts @@ -0,0 +1,227 @@ +import { + pgTable, + text, + varchar, + timestamp, + integer, + numeric, + date, + boolean, + unique, + jsonb, + uniqueIndex, pgView +} from "drizzle-orm/pg-core" +import { projects } from "./projects" +import { vendorContacts, vendors } from "./vendors" +import { eq, sql } from "drizzle-orm"; + +// ============ contracts (계약/PO 정보) ============ +export const contracts = pgTable("contracts", { + // 주 키 + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // 프로젝트와 벤더 참조 + projectId: integer("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + + vendorId: integer("vendor_id") + .notNull() + .references(() => vendors.id, { onDelete: "cascade" }), + + // 계약/PO 번호(유니크) + contractNo: varchar("contract_no", { length: 100 }).notNull().unique(), + contractName: varchar("contract_name", { length: 255 }).notNull(), + + // 계약/PO 상태나 기간 + status: varchar("status", { length: 50 }).notNull().default("ACTIVE"), + startDate: date("start_date"), // 발주일(혹은 유효 시작일) + endDate: date("end_date"), // 계약 종료일/유효 기간 등 + + // --- PO에 자주 쓰이는 필드 추가 --- + paymentTerms: text("payment_terms"), // 결제 조건(예: 30일 후 현금, 선금/잔금 등) + deliveryTerms: text("delivery_terms"), // 납품 조건(Incoterms 등) + deliveryDate: date("delivery_date"), // 납품 기한(납기 예정일) + deliveryLocation: varchar("delivery_location", { length: 255 }), // 납품 장소 + + // 가격/금액 관련 + currency: varchar("currency", { length: 10 }).default("KRW"), // 통화 (KRW, USD 등) + totalAmount: numeric("total_amount", { precision: 12, scale: 2 }), // 총 계약 금액(아이템 합산 등) + discount: numeric("discount", { precision: 12, scale: 2 }), // 전체 할인 + tax: numeric("tax", { precision: 12, scale: 2 }), // 전체 세금 + shippingFee: numeric("shipping_fee", { precision: 12, scale: 2 }), // 배송비 + netTotal: numeric("net_total", { precision: 12, scale: 2 }), // (합계) - (할인) + (세금) + (배송비) + + // 부분 납품/부분 결제 가능 여부 + partialShippingAllowed: boolean("partial_shipping_allowed").default(false), + partialPaymentAllowed: boolean("partial_payment_allowed").default(false), + + // 추가 메모/비고 + remarks: text("remarks"), + + // 버전 관리 (PO 재발행 등) + version: integer("version").default(1), + + // 생성/수정 시각 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}) + +// 타입 추론 +export type Contract = typeof contracts.$inferSelect + +// ============ contract_items (1:N 관계) ============ +// 한 계약(contracts.id)에 여러 아이템을 연결 +export const contractItems = pgTable("contract_items", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + contractId: integer("contract_id") + .notNull() + .references(() => contracts.id, { onDelete: "cascade" }), + + itemId: integer("item_id") + .notNull() + // .references(() => items.id, { onDelete: "cascade" }) + , + + // --- 품목(아이템) 단위 정보 --- + description: text("description"), // 품목 설명 (스펙, 모델명 등) + quantity: integer("quantity").notNull().default(1), + unitPrice: numeric("unit_price", { precision: 10, scale: 2 }), + + taxRate: numeric("tax_rate", { precision: 5, scale: 2 }), // % (예: 10.00) + taxAmount: numeric("tax_amount", { precision: 10, scale: 2 }), // 계산된 세금 + totalLineAmount: numeric("total_line_amount", { precision: 12, scale: 2 }), // (수량×단가±할인+세금) 등 + + // 비고 + remark: text("remark"), + + // 생성/수정 시각 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}, (t) => [ + // 같은 계약 + 같은 itemId에 대한 중복을 막음 + unique().on(t.contractId, t.itemId), + uniqueIndex("contract_items_contract_item_idx").on(t.contractId, t.itemId), +]) + +export type ContractItem = typeof contractItems.$inferSelect + +// ============ DocuSign 연동용 (전자서명 이력) ============ + +// Envelope(전자서명 요청) 테이블 +export const contractEnvelopes = pgTable("contract_envelopes", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // 연결된 계약 + contractId: integer("contract_id") + .notNull() + .references(() => contracts.id, { onDelete: "cascade" }), + + // DocuSign에서 발급되는 Envelope/Document 식별자 + envelopeId: varchar("envelope_id", { length: 200 }).notNull(), + documentId: varchar("document_id", { length: 200 }), + + // Envelope 전체 상태 (예: sent, completed, voided ...) + envelopeStatus: varchar("envelope_status", { length: 50 }), + + fileName: varchar("file_name", { length: 255 }).notNull(), + filePath: varchar("file_path", { length: 1024 }).notNull(), + // 생성/수정 시각 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// 하나의 Envelope에 여러 서명자(사인 요청 대상)가 있을 수 있음 +export const contractSigners = pgTable("contract_signers", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // Envelope와 1:N 관계 + envelopeId: integer("envelope_id") + .notNull() + .references(() => contractEnvelopes.id, { onDelete: "cascade" }), + + // Reference to vendor_contacts table (optional - if signer is from vendor contacts) + vendorContactId: integer("vendor_contact_id") + .references(() => vendorContacts.id), + + // Is this signer from the requester (company) side or vendor side + signerType: varchar("signer_type", { + length: 20, + enum: ["REQUESTER", "VENDOR"] + }).notNull().default("VENDOR"), + + // 서명자 정보 (manual entry or populated from vendor contact) + signerEmail: varchar("signer_email", { length: 255 }).notNull(), + signerName: varchar("signer_name", { length: 100 }).notNull(), + signerPosition: varchar("signer_position", { length: 100 }), + + // 서명자별 상태 (sent, delivered, signed, declined, etc.) + signerStatus: varchar("signer_status", { length: 50 }).default("PENDING"), + signedAt: timestamp("signed_at"), + + // 생성/수정 시각 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + + + +export const contractsDetailView = pgView("contracts_detail_view").as((qb) => { + return qb + .select({ + // Contract primary information + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + startDate: contracts.startDate, + endDate: contracts.endDate, + + // Project information + projectId: contracts.projectId, + projectCode: projects.code, + projectName: projects.name, + + // Vendor information + vendorId: contracts.vendorId, + vendorName: vendors.vendorName, + + // Payment and delivery details + paymentTerms: contracts.paymentTerms, + deliveryTerms: contracts.deliveryTerms, + deliveryDate: contracts.deliveryDate, + deliveryLocation: contracts.deliveryLocation, + + // Financial information + currency: contracts.currency, + totalAmount: contracts.totalAmount, + discount: contracts.discount, + tax: contracts.tax, + shippingFee: contracts.shippingFee, + netTotal: contracts.netTotal, + + // Additional settings + partialShippingAllowed: contracts.partialShippingAllowed, + partialPaymentAllowed: contracts.partialPaymentAllowed, + remarks: contracts.remarks, + version: contracts.version, + + // Timestamps + createdAt: contracts.createdAt, + updatedAt: contracts.updatedAt, + + // Electronic signature status - ADDED .as('has_signature') here + hasSignature: sql<boolean>`EXISTS ( + SELECT 1 + FROM ${contractEnvelopes} + WHERE ${contractEnvelopes.contractId} = ${contracts.id} + )`.as('has_signature'), + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) +}); + +// Type inference for the view +export type ContractDetail = typeof contractsDetailView.$inferSelect;
\ No newline at end of file diff --git a/db/schema/items.ts b/db/schema/items.ts new file mode 100644 index 00000000..1d4b0ded --- /dev/null +++ b/db/schema/items.ts @@ -0,0 +1,12 @@ +import { pgTable, varchar, text, timestamp ,serial} from "drizzle-orm/pg-core" + +export const items = pgTable("items", { + id: serial("id").primaryKey(), + itemCode: varchar("item_code", { length: 100 }).unique(), + itemName: varchar("item_name", { length: 255 }).notNull(), + description: text("description"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export type Item = typeof items.$inferSelect diff --git a/db/schema/pq.ts b/db/schema/pq.ts new file mode 100644 index 00000000..59ec8f07 --- /dev/null +++ b/db/schema/pq.ts @@ -0,0 +1,67 @@ +import { + pgTable, serial, integer, varchar, text, + timestamp, uniqueIndex +} from "drizzle-orm/pg-core"; +import { vendors } from "./vendors"; + +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 vendorPqCriteriaAnswers = pgTable("vendor_pq_criteria_answers", { + id: serial("id").primaryKey(), + vendorId: integer("vendor_id") + .notNull() + .references(() => vendors.id, { onDelete: "cascade" }), + + criteriaId: integer("criteria_id") + .notNull() + .references(() => pqCriterias.id, { onDelete: "cascade" }), + + answer: text("answer"), + + 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 diff --git a/db/schema/projects.ts b/db/schema/projects.ts new file mode 100644 index 00000000..9e253d77 --- /dev/null +++ b/db/schema/projects.ts @@ -0,0 +1,13 @@ +import { pgTable, varchar, text, timestamp ,serial} from "drizzle-orm/pg-core" + +export const projects = pgTable("projects", { + id: serial("id").primaryKey(), + code: varchar("code", { length: 50 }).notNull(), + name: text("name").notNull(), + type: varchar("type", { length: 20 }).default("ship").notNull(), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }) + + export type Project = typeof projects.$inferSelect
\ No newline at end of file diff --git a/db/schema/rfq.ts b/db/schema/rfq.ts new file mode 100644 index 00000000..98c4245c --- /dev/null +++ b/db/schema/rfq.ts @@ -0,0 +1,743 @@ +import { pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, uniqueIndex, foreignKey } from "drizzle-orm/pg-core"; +import { vendors } from "./vendors"; +import { users } from "./users"; +import { items } from "./items"; +import { eq, sql, and } from "drizzle-orm"; +import { projects } from "./projects"; + +export const rfqs = pgTable( + "rfqs", + { + id: serial("id").primaryKey(), + + // RFQ 고유 코드 + rfqCode: varchar("rfq_code", { length: 50 }).unique(), // ex) "RFQ-2025-001" + + // 프로젝트 참조 + projectId: integer("project_id") + .references(() => projects.id, { onDelete: "set null" }), + + description: varchar("description", { length: 255 }), + + dueDate: date("due_date", { mode: "date" }) + .$type<Date>() + .notNull(), + + status: varchar("status", { length: 30 }) + .$type<"DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED">() + .default("DRAFT") + .notNull(), + + // Budgetary / Purchase 여부 + rfqType: varchar("rfq_type", { length: 30 }).default("PURCHASE"), + + // 자기 자신 테이블 FK(부모 RFQ) + parentRfqId: integer("parent_rfq_id"), + + // 생성자 + createdBy: integer("created_by") + .notNull() + .references(() => users.id, { onDelete: "set null" }), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + // <==== 여기서 자기참조 FK를 정의 (수정됨) + (table) => { + return { + // parentRfqId -> rfqs.id + parentFk: foreignKey({ + columns: [table.parentRfqId], + foreignColumns: [table.id], + }).onDelete("set null"), // 메서드 체이닝 방식으로 변경 + }; + } +); + +export const rfqItems = pgTable("rfq_items", { + id: serial("id").primaryKey(), + rfqId: integer("rfq_id") + .notNull() + .references(() => rfqs.id, { onDelete: "cascade" }), + itemCode: varchar("item_code", { length: 100 }) + .notNull() + .references(() => items.itemCode, { onDelete: "cascade" }), + description: text("description"), + quantity: numeric("quantity", { precision: 12, scale: 2 }) + .$type<number>() + .default(1), + uom: varchar("uom", { length: 50 }), // 단위 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + + +export const rfqAttachments = pgTable("rfq_attachments", { + id: serial("id").primaryKey(), + rfqId: integer("rfq_id").references(() => rfqs.id), + vendorId: integer("vendor_id").references(() => vendors.id), + fileName: varchar("file_name", { length: 255 }).notNull(), + filePath: varchar("file_path", { length: 1024 }).notNull(), + evaluationId: integer("evaluation_id") + .references(() => rfqEvaluations.id) + .$type<number | null>(), + cbeId: integer("cbe_id") + .references(() => cbeEvaluations.id) + .$type<number | null>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + commentId: integer("comment_id") + .references(() => rfqComments.id) + .$type<number | null>(), +}); + +export const rfqComments = pgTable("rfq_comments", { + id: serial("id").primaryKey(), + rfqId: integer("rfq_id").references(() => rfqs.id), + vendorId: integer("vendor_id").references(() => vendors.id), + commentText: text("comment_text").notNull(), + commentedBy: integer("commented_by") + .references(() => users.id) // users 테이블의 id 필드 참조 추가 + .notNull(), + // 아래처럼, 평가(TBE/CBE)에 속한 코멘트인지 여부를 구분할 필드 + evaluationId: integer("evaluation_id") + .references(() => rfqEvaluations.id) + .$type<number | null>(), + cbeId: integer("cbe_id") + .references(() => cbeEvaluations.id) + .$type<number | null>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const rfqEvaluations = pgTable("rfq_evaluations", { + id: serial("id").primaryKey(), + rfqId: integer("rfq_id") + .notNull() + .references(() => rfqs.id), + vendorId: integer("vendor_id") + .notNull() + .references(() => vendors.id), + evalType: varchar("eval_type", { length: 30 }) + .$type<"TBE" | "CBE">(), + // TBE or CBE (기술 / 상업) + result: varchar("result", { length: 255 }), // 예: "PASS", "FAIL", "ACCEPTABLE" 등 + notes: text("notes"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + + +// General vendor response to RFQ invitation +export const vendorResponses = pgTable( + "vendor_responses", + { + id: serial("id").primaryKey(), + + rfqId: integer("rfq_id") + .notNull() + .references(() => rfqs.id, { onDelete: "cascade" }), + + vendorId: integer("vendor_id") + .notNull() + .references(() => vendors.id, { onDelete: "cascade" }), + + // Vendor's overall response status + responseStatus: varchar("response_status", { length: 30 }) + .$type<"INVITED" | "ACCEPTED" | "DECLINED" | "REVIEWING" | "RESPONDED">() + .default("REVIEWING") + .notNull(), + + // General notes from vendor + notes: text("notes"), + + // Metadata + respondedBy: varchar("responded_by", { length: 255 }), // Could be vendor contact name + respondedAt: timestamp("responded_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (table) => ({ + // Each vendor can only have one response per RFQ + uniqueConstraint: uniqueIndex("vendor_response_unique").on( + table.rfqId, + table.vendorId + ), + }) +); + +// Technical response details from vendor +export const vendorTechnicalResponses = pgTable( + "vendor_technical_responses", + { + id: serial("id").primaryKey(), + + // Link to main response + responseId: integer("response_id") + .notNull() + .references(() => vendorResponses.id, { onDelete: "cascade" }), + + // 간소화된 기술 응답 필드 + // 기술 내용은 주로 첨부 파일로 받으므로 최소한의 필드만 유지 + summary: text("summary"), // 간단한 기술 응답 요약 + notes: text("notes"), // 추가 노트나 코멘트 + + // Metadata + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + } +); + +// Commercial response details from vendor +export const vendorCommercialResponses = pgTable( + "vendor_commercial_responses", + { + id: serial("id").primaryKey(), + + // Link to main response + responseId: integer("response_id") + .notNull() + .references(() => vendorResponses.id, { onDelete: "cascade" }), + + // Commercial response fields + totalPrice: numeric("total_price", { precision: 18, scale: 2 }).$type<number>(), + currency: varchar("currency", { length: 10 }).default("USD"), + + // Commercial terms + paymentTerms: varchar("payment_terms", { length: 255 }), + incoterms: varchar("incoterms", { length: 50 }), + deliveryPeriod: varchar("delivery_period", { length: 100 }), + warrantyPeriod: varchar("warranty_period", { length: 100 }), + + // Validity of offer + validityPeriod: varchar("validity_period", { length: 100 }), + + // Price breakdown or other commercial notes + priceBreakdown: text("price_breakdown"), + commercialNotes: text("commercial_notes"), + + // Metadata + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + } +); + + +// Attachment table for vendor responses +export const vendorResponseAttachments = pgTable( + "vendor_response_attachments", + { + id: serial("id").primaryKey(), + + // Can be linked to any type of response + responseId: integer("response_id") + .references(() => vendorResponses.id, { onDelete: "cascade" }), + technicalResponseId: integer("technical_response_id") + .references(() => vendorTechnicalResponses.id, { onDelete: "cascade" }), + commercialResponseId: integer("commercial_response_id") + .references(() => vendorCommercialResponses.id, { onDelete: "cascade" }), + + // File details + fileName: varchar("file_name", { length: 255 }).notNull(), + filePath: varchar("file_path", { length: 1024 }).notNull(), + fileType: varchar("file_type", { length: 50 }), + + // 첨부 파일 분류 + attachmentType: varchar("attachment_type", { length: 50 }) + .$type<"TECHNICAL_SPEC" | "QUALITY_CERT" | "COMPLIANCE_DOC" | "COMMERCIAL" | "OTHER">(), + description: varchar("description", { length: 255 }), + + // Metadata + uploadedAt: timestamp("uploaded_at").defaultNow().notNull(), + uploadedBy: varchar("uploaded_by", { length: 255 }), + } +); + + +export const cbeEvaluations = pgTable("cbe_evaluations", { + id: serial("id").primaryKey(), + + // 어떤 RFQ에 대한 CBE인지 + rfqId: integer("rfq_id") + .notNull() + .references(() => rfqs.id, { onDelete: "cascade" }), + + // 어떤 벤더의 상업평가(CBE)인지 + vendorId: integer("vendor_id") + .notNull() + .references(() => vendors.id, { onDelete: "cascade" }), + + // 평가자(작성자) 정보가 필요하다면 (예: 상업팀 담당자) + evaluatedBy: integer("evaluated_by") + .references(() => users.id, { onDelete: "set null" }), + + // 평가 일시 + evaluatedAt: timestamp("evaluated_at").defaultNow().notNull(), + + // 상업평가 결과(예: ACCEPT, REJECT, PENDING 등) + result: varchar("result", { length: 50 }), + + // 총 금액(견적 기준일 수도 있고, 평가 과정에서 조정이 있을 수도 있음) + totalCost: numeric("total_cost", { precision: 18, scale: 2 }).$type<number>(), + + // 통화 + currency: varchar("currency", { length: 10 }).default("USD"), + + // 결제 조건, 인도 조건, 기타 상업 조건 + paymentTerms: varchar("payment_terms", { length: 255 }), + incoterms: varchar("incoterms", { length: 50 }), + deliverySchedule: text("delivery_schedule"), // 정해진 형식이 있다면 varchar, 아니면 text + + // 기타 비고 + notes: text("notes"), + + // 기본 관리 필드 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + + + +export const cbeView = pgView("cbe_view").as((qb) => { + return qb + .select({ + cbeId: sql<number>`${cbeEvaluations.id}`.as("cbe_id"), + rfqId: sql<number>`${cbeEvaluations.rfqId}`.as("rfq_id"), + vendorId: sql<number>`${cbeEvaluations.vendorId}`.as("vendor_id"), + totalCost: sql<number | null>`${cbeEvaluations.totalCost}`.as("total_cost"), + currency: sql<string | null>`${cbeEvaluations.currency}`.as("currency"), + paymentTerms: sql<string | null>`${cbeEvaluations.paymentTerms}`.as("payment_terms"), + incoterms: sql<string | null>`${cbeEvaluations.incoterms}`.as("incoterms"), + result: sql<string | null>`${cbeEvaluations.result}`.as("result"), + notes: sql<string | null>`${cbeEvaluations.notes}`.as("notes"), + evaluatedBy: sql<number | null>`${cbeEvaluations.evaluatedBy}`.as("evaluated_by"), + evaluatedAt: sql<Date>`${cbeEvaluations.evaluatedAt}`.as("evaluated_at"), + + rfqCode: sql<string>`${rfqs.rfqCode}`.as("rfq_code"), + rfqDescription: sql<string | null>`${rfqs.description}`.as("rfq_description"), + vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"), + vendorCode: sql<string | null>`${vendors.vendorCode}`.as("vendor_code"), + + projectId: sql<number | null>`${projects.id}`.as("project_id"), + projectCode: sql<string | null>`${projects.code}`.as("project_code"), + projectName: sql<string | null>`${projects.name}`.as("project_name"), + + evaluatorName: sql<string | null>`${users.name}`.as("evaluator_name"), + evaluatorEmail: sql<string | null>`${users.email}`.as("evaluator_email"), + + }) + .from(cbeEvaluations) + .innerJoin(rfqs, eq(cbeEvaluations.rfqId, rfqs.id)) + .innerJoin(vendors, eq(cbeEvaluations.vendorId, vendors.id)) + .leftJoin(projects, eq(rfqs.projectId, projects.id)) + .leftJoin(users, eq(cbeEvaluations.evaluatedBy, users.id)); +}); + + +//view + +export const rfqsView = pgView("rfqs_view").as((qb) => { + return qb + .select({ + // rfqs 주요 필드 + id: sql<number>`${rfqs.id}`.as("rfq_id"), + status: sql<string>`${rfqs.status}`.as("status"), + createdAt: sql<Date>`${rfqs.createdAt}`.as("created_at"), + updatedAt: sql<Date>`${rfqs.updatedAt}`.as("updated_at"), + createdBy: sql<number | null>`${rfqs.createdBy}`.as("created_by"), + rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"), + + // rfqs 추가 필드 + rfqCode: sql<string>`${rfqs.rfqCode}`.as("rfq_code"), + description: sql<string | null>`${rfqs.description}`.as("description"), + dueDate: sql<Date | null>`${rfqs.dueDate}`.as("due_date"), + parentRfqId: sql<number | null>`${rfqs.parentRfqId}`.as("parent_rfq_id"), + + // 프로젝트 테이블 필드 + projectId: sql<number | null>`${projects.id}`.as("project_id"), + projectCode: sql<string | null>`${projects.code}`.as("project_code"), + projectName: sql<string | null>`${projects.name}`.as("project_name"), + + // users 테이블 관련 필드 + userEmail: sql<string | null>`${users.email}`.as("user_email"), + userName: sql<string | null>`${users.name}`.as("user_name"), + + // rfq_items 및 rfq_attachments 카운트 + itemCount: sql<number>`( + SELECT COUNT(*) + FROM "rfq_items" + WHERE "rfq_items"."rfq_id" = ${rfqs.id} + )`.as("item_count"), + + attachmentCount: sql<number>`( + SELECT COUNT(*) + FROM "rfq_attachments" + WHERE "rfq_attachments"."rfq_id" = ${rfqs.id} + )`.as("attachment_count"), + + + }) + .from(rfqs) + .leftJoin(projects, eq(rfqs.projectId, projects.id)) + .leftJoin(users, eq(rfqs.createdBy, users.id)) +}) + +// vendorRfqView 업데이트 벤더 기준 +export const vendorRfqView = pgView("vendor_rfq_view").as((qb) => { + return qb + .select({ + // vendors 테이블 + vendorId: sql<number>`${vendors.id}`.as("vendor_id"), + vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"), + vendorCode: sql<string>`${vendors.vendorCode}`.as("vendor_code"), + address: sql<string | null>`${vendors.address}`.as("address"), + country: sql<string | null>`${vendors.country}`.as("country"), + email: sql<string | null>`${vendors.email}`.as("email"), + website: sql<string | null>`${vendors.website}`.as("website"), + vendorStatus: sql<string>`${vendors.status}`.as("vendor_status"), + + // rfqVendors 테이블 + rfqId: sql<number | null>`${vendorResponses.rfqId}`.as("rfq_id"), + rfqVendorStatus: sql<string | null>`${vendorResponses.responseStatus}`.as("rfq_vendor_status"), + rfqVendorUpdated: sql<Date | null>`${vendorResponses.updatedAt}`.as("rfq_vendor_updated"), + + // rfqs 테이블 + rfqCode: sql<string | null>`${rfqs.rfqCode}`.as("rfq_code"), + description: sql<string | null>`${rfqs.description}`.as("description"), + dueDate: sql<Date | null>`${rfqs.dueDate}`.as("due_date"), + + // 프로젝트 테이블 (업데이트됨) + projectId: sql<number | null>`${projects.id}`.as("project_id"), + projectCode: sql<string | null>`${projects.code}`.as("project_code"), + projectName: sql<string | null>`${projects.name}`.as("project_name"), + }) + .from(vendors) + .leftJoin(vendorResponses, eq(vendorResponses.vendorId, vendors.id)) + .leftJoin(rfqs, eq(vendorResponses.rfqId, rfqs.id)) + .leftJoin(projects, eq(rfqs.projectId, projects.id)) // 프로젝트 테이블 조인 추가 +}) + + + +// vendorTbeView 업데이트 +export const vendorTbeView = pgView("vendor_tbe_view").as((qb) => { + return qb + .select({ + // vendors 기본정보 + vendorId: sql<number>`${vendors.id}`.as("vendor_id"), + vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"), + vendorCode: sql<string | null>`${vendors.vendorCode}`.as("vendor_code"), + address: sql<string | null>`${vendors.address}`.as("address"), + country: sql<string | null>`${vendors.country}`.as("country"), + email: sql<string | null>`${vendors.email}`.as("email"), + website: sql<string | null>`${vendors.website}`.as("website"), + vendorStatus: sql<string>`${vendors.status}`.as("vendor_status"), + + // rfq_vendors + vendorResponseId: sql<number | null>`${vendorResponses.id}`.as("vendor_response_id"), + rfqId: sql<number | null>`${vendorResponses.rfqId}`.as("rfq_id"), + rfqVendorStatus: sql<string | null>`${vendorResponses.responseStatus}`.as("rfq_vendor_status"), + rfqVendorUpdated: sql<Date | null>`${vendorResponses.updatedAt}`.as("rfq_vendor_updated"), + + // rfqs + rfqCode: sql<string | null>`${rfqs.rfqCode}`.as("rfq_code"), + rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"), + description: sql<string | null>`${rfqs.description}`.as("description"), + dueDate: sql<Date | null>`${rfqs.dueDate}`.as("due_date"), + + // 프로젝트 테이블 필드 (업데이트됨) + projectId: sql<number | null>`${projects.id}`.as("project_id"), + projectCode: sql<string | null>`${projects.code}`.as("project_code"), + projectName: sql<string | null>`${projects.name}`.as("project_name"), + + // rfq_evaluations (TBE 전용) + tbeId: sql<number | null>`${rfqEvaluations.id}`.as("tbe_id"), + tbeResult: sql<string | null>`${rfqEvaluations.result}`.as("tbe_result"), + tbeNote: sql<string | null>`${rfqEvaluations.notes}`.as("tbe_note"), + tbeUpdated: sql<Date | null>`${rfqEvaluations.updatedAt}`.as("tbe_updated"), + }) + .from(vendors) + .leftJoin( + vendorResponses, + eq(vendorResponses.vendorId, vendors.id) + ) + // 1) rfqVendors ↔ rfqs + .leftJoin( + rfqs, + eq(vendorResponses.rfqId, rfqs.id) + ) + // 2) 프로젝트 테이블 조인 추가 + .leftJoin( + projects, + eq(rfqs.projectId, projects.id) + ) + // 3) rfqEvaluations + .leftJoin( + rfqEvaluations, + and( + eq(rfqEvaluations.vendorId, vendors.id), + eq(rfqEvaluations.evalType, "TBE"), + eq(rfqEvaluations.rfqId, vendorResponses.rfqId) + ) + ) +}) + +export const vendorCbeView = pgView("vendor_cbe_view").as((qb) => { + return qb + .select({ + // [1] Vendor 기본정보 + vendorId: sql<number>`${vendors.id}`.as("vendor_id"), + vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"), + vendorCode: sql<string | null>`${vendors.vendorCode}`.as("vendor_code"), + address: sql<string | null>`${vendors.address}`.as("address"), + country: sql<string | null>`${vendors.country}`.as("country"), + email: sql<string | null>`${vendors.email}`.as("email"), + website: sql<string | null>`${vendors.website}`.as("website"), + vendorStatus: sql<string>`${vendors.status}`.as("vendor_status"), + + // [2] rfq_vendors (vendorResponses) + vendorResponseId: sql<number | null>`${vendorResponses.id}`.as("vendor_response_id"), + rfqId: sql<number | null>`${vendorResponses.rfqId}`.as("rfq_id"), + rfqVendorStatus: sql<string | null>`${vendorResponses.responseStatus}`.as("rfq_vendor_status"), + rfqVendorUpdated: sql<Date | null>`${vendorResponses.updatedAt}`.as("rfq_vendor_updated"), + + // [3] rfqs + rfqCode: sql<string | null>`${rfqs.rfqCode}`.as("rfq_code"), + rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"), + description: sql<string | null>`${rfqs.description}`.as("description"), + dueDate: sql<Date | null>`${rfqs.dueDate}`.as("due_date"), + + // [4] 프로젝트 정보 + projectId: sql<number | null>`${projects.id}`.as("project_id"), + projectCode: sql<string | null>`${projects.code}`.as("project_code"), + projectName: sql<string | null>`${projects.name}`.as("project_name"), + + // [5] CBE 평가(cbeEvaluations) + cbeId: sql<number | null>`${cbeEvaluations.id}`.as("cbe_id"), + cbeResult: sql<string | null>`${cbeEvaluations.result}`.as("cbe_result"), + cbeNote: sql<string | null>`${cbeEvaluations.notes}`.as("cbe_note"), + cbeUpdated: sql<Date | null>`${cbeEvaluations.updatedAt}`.as("cbe_updated"), + + // 상업평가용 추가 필드들 + totalCost: sql<number | null>`${cbeEvaluations.totalCost}`.as("total_cost"), + currency: sql<string | null>`${cbeEvaluations.currency}`.as("currency"), + paymentTerms: sql<string | null>`${cbeEvaluations.paymentTerms}`.as("payment_terms"), + incoterms: sql<string | null>`${cbeEvaluations.incoterms}`.as("incoterms"), + deliverySchedule: sql<string | null>`${cbeEvaluations.deliverySchedule}`.as("delivery_schedule"), + }) + .from(vendors) + .leftJoin( + vendorResponses, + eq(vendorResponses.vendorId, vendors.id) + ) + .leftJoin( + rfqs, + eq(vendorResponses.rfqId, rfqs.id) + ) + .leftJoin( + projects, + eq(rfqs.projectId, projects.id) + ) + .leftJoin( + cbeEvaluations, + and( + eq(cbeEvaluations.vendorId, vendors.id), + eq(cbeEvaluations.rfqId, vendorResponses.rfqId) + ) + ); + }); + +// Types for TypeScript +export type VendorResponse = typeof vendorResponses.$inferSelect; +export type VendorTechnicalResponse = typeof vendorTechnicalResponses.$inferSelect; +export type VendorCommercialResponse = typeof vendorCommercialResponses.$inferSelect; +export type VendorResponseAttachment = typeof vendorResponseAttachments.$inferSelect; + + +// View to combine vendor response data with projects +export const vendorResponsesView = pgView("vendor_responses_view").as((qb) => { + return qb + .select({ + // Response identification + responseId: sql<number>`${vendorResponses.id}`.as("response_id"), + rfqId: sql<number>`${vendorResponses.rfqId}`.as("rfq_id"), + vendorId: sql<number>`${vendorResponses.vendorId}`.as("vendor_id"), + + // RFQ information + rfqCode: sql<string>`${rfqs.rfqCode}`.as("rfq_code"), + rfqDescription: sql<string | null>`${rfqs.description}`.as("rfq_description"), + rfqDueDate: sql<Date | null>`${rfqs.dueDate}`.as("rfq_due_date"), + rfqStatus: sql<string>`${rfqs.status}`.as("rfq_status"), + rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"), + rfqCreatedAt: sql<Date>`${rfqs.createdAt}`.as("rfq_created_at"), + rfqUpdatedAt: sql<Date>`${rfqs.updatedAt}`.as("rfq_updated_at"), + rfqCreatedBy: sql<number | null>`${rfqs.createdBy}`.as("rfq_created_by"), + + // Project information + projectId: sql<number | null>`${projects.id}`.as("project_id"), + projectCode: sql<string | null>`${projects.code}`.as("project_code"), + projectName: sql<string | null>`${projects.name}`.as("project_name"), + + // Vendor information + vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"), + vendorCode: sql<string | null>`${vendors.vendorCode}`.as("vendor_code"), + + // Response status + responseStatus: sql<"INVITED" | "ACCEPTED" | "DECLINED" | "REVIEWING" | "RESPONDED">`${vendorResponses.responseStatus}` + .as("response_status"), + respondedAt: sql<Date>`${vendorResponses.respondedAt}`.as("responded_at"), + + // Technical response indicators + hasTechnicalResponse: sql<boolean>`CASE WHEN ${vendorTechnicalResponses.id} IS NOT NULL THEN TRUE ELSE FALSE END`.as("has_technical_response"), + technicalResponseId: sql<number | null>`${vendorTechnicalResponses.id}`.as("technical_response_id"), + + // Commercial response indicators + hasCommercialResponse: sql<boolean>`CASE WHEN ${vendorCommercialResponses.id} IS NOT NULL THEN TRUE ELSE FALSE END`.as("has_commercial_response"), + commercialResponseId: sql<number | null>`${vendorCommercialResponses.id}`.as("commercial_response_id"), + totalPrice: sql<number | null>`${vendorCommercialResponses.totalPrice}`.as("total_price"), + currency: sql<string | null>`${vendorCommercialResponses.currency}`.as("currency"), + + // Evaluation links + tbeId: sql<number | null>`${rfqEvaluations.id}`.as("tbe_id"), + tbeResult: sql<string | null>`${rfqEvaluations.result}`.as("tbe_result"), + cbeId: sql<number | null>`${cbeEvaluations.id}`.as("cbe_id"), + cbeResult: sql<string | null>`${cbeEvaluations.result}`.as("cbe_result"), + + // Attachments count + attachmentCount: sql<number>`( + SELECT COUNT(*) + FROM "vendor_response_attachments" + WHERE "vendor_response_attachments"."response_id" = ${vendorResponses.id} + )`.as("attachment_count"), + }) + .from(vendorResponses) + .innerJoin(rfqs, eq(vendorResponses.rfqId, rfqs.id)) + .innerJoin(vendors, eq(vendorResponses.vendorId, vendors.id)) + .leftJoin(projects, eq(rfqs.projectId, projects.id)) // Added project join + .leftJoin( + vendorTechnicalResponses, + eq(vendorTechnicalResponses.responseId, vendorResponses.id) + ) + .leftJoin( + vendorCommercialResponses, + eq(vendorCommercialResponses.responseId, vendorResponses.id) + ) + .leftJoin( + rfqEvaluations, + and( + eq(rfqEvaluations.rfqId, vendorResponses.rfqId), + eq(rfqEvaluations.vendorId, vendorResponses.vendorId), + eq(rfqEvaluations.evalType, "TBE") + ) + ) + .leftJoin( + cbeEvaluations, + and( + eq(cbeEvaluations.rfqId, vendorResponses.rfqId), + eq(cbeEvaluations.vendorId, vendorResponses.vendorId) + ) + ); +}); + + +export type VendorResponsesView = typeof vendorResponsesView.$inferSelect + + +export type Rfq = typeof rfqs.$inferSelect +export type RfqItem = typeof rfqItems.$inferSelect +export type RfqAttach = typeof rfqAttachments.$inferSelect +export type RfqComment = typeof rfqComments.$inferSelect +export type RfqEvaluation = typeof rfqEvaluations.$inferSelect + +export type VendorRfqViewBase = typeof vendorRfqView.$inferSelect + +// 2) 우리가 코드에서 필요한 속성까지 합친 확장 타입 +export interface VendorRfqViewWithComments extends VendorRfqViewBase { + comments?: { + id: number + commentText: string + vendorId?: number + evaluationId?: number + createdAt: Date + commentedBy?: number + }[] + // 필요하다면 quote, attachments 등 추가 필드도... +} + +export type VendorTbeView = typeof vendorTbeView.$inferSelect +export type VendorCbeView = typeof vendorCbeView.$inferSelect + +export interface RfqWithItemCount { + // 기본 RFQ 식별자 + rfqId: number; + // id: number; // rfqId와 동일 (편의상 중복) + + // RFQ 기본 정보 + rfqCode: string; + description: string | null; + status: string; // 필요시 "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED" 로 제한 가능 + + // 날짜 정보 + dueDate: Date | null; + createdAt: Date; + updatedAt: Date; + + // 프로젝트 관련 정보 + projectCode: string | null; + projectName: string | null; + + // 생성자 정보 + createdBy: number | null; + createdByEmail: string | null; + + // 사용자 정보 + userId?: number | null; + userEmail?: string | null; + userName?: string | null; + + // 집계 정보 (서브쿼리로 계산됨) + itemCount: number; + attachCount: number; + + // 추가 필드 (필요시 데이터 변환 함수에서 채울 수 있음) + rfqType?: string | null; + projectId?: number | null; + parentRfqId?: number | null; + + // 다른 필요한 필드들 + vendorId?: number | null; + rfqVendorId?: number | null; + rfqVendorStatus?: string | null; + rfqVendorUpdated?: Date | null; +} + +export interface RfqWithItems extends Rfq { + lines: RfqItem[]; +} + +export interface RfqWithAll extends VendorResponsesView { + + // 아래처럼 배열/객체로 + items: Array<{ + id: number + itemCode: string + itemName: string + quantity?: number + uom?: string + description?: string | null + }> + attachments: Array<{ + id: number + fileName: string + filePath: string + vendorId?: number | null + evaluationId?: number | null + }> + comments: Array<{ + id: number + commentText: string + vendorId?: number | null + evaluationId?: number | null + createdAt: Date + }> + +} +export type RfqsView = typeof rfqsView.$inferSelect diff --git a/db/schema/tasks.ts b/db/schema/tasks.ts new file mode 100644 index 00000000..fc4000bb --- /dev/null +++ b/db/schema/tasks.ts @@ -0,0 +1,46 @@ +import { integer, pgTable, boolean, timestamp, varchar, primaryKey } from "drizzle-orm/pg-core"; +import { sql } from "drizzle-orm" + +import { generateId } from "@/lib/id" + +export const tasks = pgTable("tasks", { + id: varchar("id", { length: 30 }) + .$defaultFn(() => generateId()) + .primaryKey(), + code: varchar("code", { length: 128 }) + .notNull() + .unique() + .default( + sql`concat('TASK-', to_char(nextval('tasks_code_seq'), 'FM0000'))` + ) + , + title: varchar("title", { length: 128 }), + status: varchar("status", { + length: 30, + enum: ["todo", "in-progress", "done", "canceled"], + }) + .notNull() + .default("todo"), + label: varchar("label", { + length: 30, + enum: ["bug", "feature", "enhancement", "documentation"], + }) + .notNull() + .default("bug"), + priority: varchar("priority", { + length: 30, + enum: ["low", "medium", "high"], + }) + .notNull() + .default("low"), + archived: boolean("archived").notNull().default(false), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .default(sql`current_timestamp`) + .$onUpdate(() => new Date()), +}) + +export type Task = typeof tasks.$inferSelect +export type NewTask = Omit<Task, "code"> & { + code?: string +}
\ No newline at end of file diff --git a/db/schema/users.ts b/db/schema/users.ts new file mode 100644 index 00000000..843ee2f3 --- /dev/null +++ b/db/schema/users.ts @@ -0,0 +1,147 @@ +import { integer, pgTable, varchar,timestamp,pgEnum ,pgView, text, primaryKey} from "drizzle-orm/pg-core"; +import { eq , sql} from "drizzle-orm"; +import { vendors } from "./vendors"; + +export const userDomainEnum = pgEnum("user_domain", ["evcp", "partners"]); + + +export const users = pgTable("users", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + name: varchar("name", { length: 255 }).notNull(), + email: varchar("email", { length: 255 }).notNull().unique(), + companyId: integer("company_id") + .references(() => vendors.id, { onDelete: "set null" }), + domain: userDomainEnum("domain").notNull().default("partners"), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + imageUrl: varchar("image_url", { length: 1024 }), + +}); + +export const otps = pgTable('otps', { + email: varchar('email', { length: 256 }).notNull().primaryKey(), + code: varchar('code', { length: 6 }).notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + otpToken: varchar({ length: 512 }).notNull(), // 이제 null 불가능 + otpExpires: timestamp('otp_expires').notNull(), // null 불가능 +}); + +export const permissions = pgTable("permissions", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + permissionKey: text("permission_key").notNull(), + description: text("description"), + createdAt: timestamp("created_at").default(sql`now()`), +}); + +export const roles = pgTable("roles", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + name: text("name").notNull(), // admin, manager + domain: userDomainEnum("domain").notNull(), + companyId: integer("company_id") + .references(() => vendors.id, { onDelete: "cascade" }), + description: text("description").default("").notNull(), + createdAt: timestamp("created_at").default(sql`now()`), +}); + +export const rolePermissions = pgTable("role_permissions", { + roleId: integer("role_id") + .references(() => roles.id, { onDelete: "cascade" }) + .notNull(), + permissionId: integer("permission_id") + .references(() => permissions.id, { onDelete: "cascade" }) + .notNull(), +}, (table) => { + return [{ + pk: primaryKey({ columns: [table.roleId, table.permissionId] }), + pkWithCustomName: primaryKey({ name: 'rolePermissions_pk', columns: [table.roleId, table.permissionId] }), + }]; +}); + +export const userRoles = pgTable("user_roles", { + userId: integer("user_id") + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), + roleId: integer("role_id") + .references(() => roles.id, { onDelete: "cascade" }) + .notNull(), +}, (table) => { + return [{ + pk: primaryKey({ columns: [table.userId, table.roleId] }), + pkWithCustomName: primaryKey({ name: 'userRoles_pk', columns: [table.userId, table.roleId] }), + }]; +}); + +export type User = typeof users.$inferSelect +export type UserRole = typeof userRoles.$inferSelect +export type Role = typeof roles.$inferSelect +export type UserDomainType = (typeof userDomainEnum.enumValues)[number]; + + +export const userView = pgView("user_view").as((qb) => { + return qb + .select({ + // 1) userId: number + user_id: sql<number>`${users.id}`.as("user_id"), + + // 2) userName: string + user_name: sql<string>`${users.name}`.as("user_name"), + + // 3) userEmail: string + user_email: sql<string>`${users.email}`.as("user_email"), + + user_domain: sql<string>`${users.domain}`.as("user_domain"), + + user_image: sql<string>`${users.imageUrl}`.as("user_image"), + + // 4) companyId: number | null + company_id: sql<number | null>`${vendors.id}`.as("company_id"), + + // 5) companyName: string | null + company_name: sql<string | null>`${vendors.vendorName}`.as("company_name"), + + // 6) roles: string[] + // Drizzle가 이 배열을 제대로 추론하려면 sql<string[]> 붙여야 함 + roles: sql<string[]>` + array_agg(${roles.name}) + `.as("roles"), + // 7) createdAt: Date + created_at: sql<Date>`${users.createdAt}`.as("created_at"), + }) + .from(users) + .leftJoin(vendors, eq(users.companyId, vendors.id)) + .leftJoin(userRoles, eq(users.id, userRoles.userId)) + .leftJoin(roles, eq(userRoles.roleId, roles.id)) + // array_agg를 쓰려면 GROUP BY 필요 + .groupBy( + users.id, + vendors.id + // user_roles 테이블은 array_agg로 묶이므로 groupBy 필요 X + ); +}); + +export const roleView = pgView("role_view").as((qb) => { + return qb + .select({ + id: sql<number >`${roles.id}`.as("id"), + name: sql<string>`${roles.name}`.as("name"), + description: sql<string>`${roles.description}`.as("description"), + domain: sql<string>`${roles.domain}`.as("domain"), + created_at: sql<Date>`${roles.createdAt}`.as("created_at"), + // company + company_id: sql<number | null>`${vendors.id}`.as("company_id"), + company_name: sql<string | null>`${vendors.vendorName}`.as("company_name"), + + // userCount + user_count: sql<number>`COUNT(${users.id})`.as("user_count"), + }) + .from(roles) + .leftJoin(userRoles, eq(userRoles.roleId, roles.id)) + .leftJoin(users, eq(users.id, userRoles.userId)) + .leftJoin(vendors, eq(roles.companyId, vendors.id)) + .groupBy(roles.id, vendors.id) // user_count(집계) 위해 groupBy +}) + +export type UserView = typeof userView.$inferSelect; +export type RoleView = typeof roleView.$inferSelect; +export type RolePermission = typeof rolePermissions.$inferSelect;
\ No newline at end of file diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts new file mode 100644 index 00000000..f7baa883 --- /dev/null +++ b/db/schema/vendorData.ts @@ -0,0 +1,205 @@ +import { + pgTable, + text, + varchar, + timestamp, + integer, numeric, date, unique, serial, jsonb, uniqueIndex +} from "drizzle-orm/pg-core" +import { contractItems } from "./contract" + +export const forms = pgTable("forms", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + contractItemId: integer("contract_item_id") + .notNull() + .references(() => contractItems.id, { onDelete: "cascade" }), + formCode: varchar("form_code", { length: 100 }).notNull(), + formName: varchar("form_name", { length: 255 }).notNull(), + // tagType: varchar("tag_type", { length: 50 }).notNull(), + // class: varchar("class", { length: 100 }).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}, (table) => { + return { + // contractItemId와 formCode의 조합을 유니크하게 설정 + contractItemFormCodeUnique: uniqueIndex("contract_item_form_code_unique").on( + table.contractItemId, + table.formCode + ), + } +}) + +export const rfqAttachments = pgTable("form_templates", { + id: serial("id").primaryKey(), + formId: integer("form_id").references(() => forms.id), + fileName: varchar("file_name", { length: 255 }).notNull(), + filePath: varchar("file_path", { length: 1024 }).notNull(), + + createdAt: timestamp("created_at").defaultNow().notNull(), + udpatedAt: timestamp("updated_at").defaultNow().notNull(), + +}); + + +export const formMetas = pgTable("form_metas", { + id: serial("id").primaryKey(), + formCode: varchar("form_code", { length: 50 }).notNull(), + formName: varchar("form_name", { length: 255 }).notNull(), + columns: jsonb("columns").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), +}) + +export const formEntries = pgTable("form_entries", { + id: serial("id").primaryKey(), + formCode: varchar("form_code", { length: 50 }).notNull(), + data: jsonb("data").notNull(), + contractItemId: integer("contract_item_id") + .notNull() + .references(() => contractItems.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), +}) + + +// ============ tags (각 계약 아이템에 대한 Tag) ============ +// "어느 계약의 어느 아이템에 대한 태그"임을 나타내려면 contract_items를 참조 +export const tags = pgTable("tags", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // 이 Tag가 속한 "계약 내 아이템" (즉 contract_items.id) + contractItemId: integer("contract_item_id") + .notNull() + .references(() => contractItems.id, { onDelete: "cascade" }), + + formId: integer("form_id") + .references(() => forms.id, { onDelete: "set null" }), + + tagNo: varchar("tag_no", { length: 100 }).notNull(), + tagType: varchar("tag_type", { length: 50 }).notNull(), + class: varchar("class", { length: 100 }).notNull(), + description: text("description"), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}) + +export type Tag = typeof tags.$inferSelect +export type Form = typeof forms.$inferSelect +export type NewTag = typeof tags.$inferInsert + +export const tagTypes = pgTable("tag_types", { + code: varchar("code", { length: 50 }).primaryKey(), + description: text("description").notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), +}) + +export const tagSubfields = pgTable("tag_subfields", { + id: serial("id").primaryKey(), + + // 외래키: tagTypeCode -> tagTypes.code + tagTypeCode: varchar("tag_type_code", { length: 50 }) + .notNull() + .references(() => tagTypes.code, { onDelete: "cascade" }), + + /** + * 나머지 필드 + */ + // tagTypeDescription: -> 이제 불필요. tagTypes.description로 join + attributesId: varchar("attributes_id", { length: 50 }).notNull(), + attributesDescription: text("attributes_description").notNull(), + + expression: text("expression"), + delimiter: varchar("delimiter", { length: 10 }), + + sortOrder: integer("sort_order").default(0).notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), +}, (table) => { + return { + uniqTagTypeAttribute: unique("uniq_tag_type_attribute").on( + table.tagTypeCode, + table.attributesId + ), + }; +}); + +export const tagSubfieldOptions = pgTable("tag_subfield_options", { + id: serial("id").primaryKey(), + + // 어떤 subfield에 속하는 옵션인지 + attributesId: varchar("attributes_id", { length: 50 }) + .notNull() + .references(() => tagSubfields.attributesId, { onDelete: "cascade" }), + + /** + * 실제 코드 (예: "PM", "AA", "VB", "VAR", "01", "02" ...) + */ + code: varchar("code", { length: 50 }).notNull(), + + /** + * 사용자에게 보여줄 레이블 (예: "Pump", "Pneumatic Motor", "Ball Valve", ...) + */ + label: text("label").notNull(), + + /** + * 생성/수정 시각 (선택) + */ + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), +}) + +export const tagClasses = pgTable("tag_classes", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // 기존 code/label + code: varchar("code", { length: 100 }).notNull(), + label: text("label").notNull(), + + // 새 필드: tagTypeCode -> references tagTypes.code + tagTypeCode: varchar("tag_type_code", { length: 50 }) + .notNull() + .references(() => tagTypes.code, { onDelete: "cascade" }), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}) + +export const tagTypeClassFormMappings = pgTable("tag_type_class_form_mappings", { + id: serial("id").primaryKey(), + + tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(), + classLabel: varchar("class_label", { length: 255 }).notNull(), + + formCode: varchar("form_code", { length: 50 }).notNull(), + formName: varchar("form_name", { length: 255 }).notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), +}) + +export type TagTypeClassFormMappings = typeof tagTypeClassFormMappings.$inferSelect +export type TagSubfields = typeof tagSubfields.$inferSelect +export type TagSubfieldOption = typeof tagSubfieldOptions.$inferSelect +export type TagClasses = typeof tagClasses.$inferSelect + + +export const viewTagSubfields = pgTable("view_tag_subfields", { + id: integer("id").primaryKey(), + + tagTypeCode: varchar("tag_type_code", { length: 50 }).notNull(), + tagTypeDescription: text("tag_type_description"), + attributesId: varchar("attributes_id", { length: 50 }).notNull(), + attributesDescription: text("attributes_description").notNull(), + + expression: text("expression"), + delimiter: varchar("delimiter", { length: 10 }), + sortOrder: integer("sort_order").default(0).notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }), + updatedAt: timestamp("updated_at", { withTimezone: true }), +}) + +export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect diff --git a/db/schema/vendorDocu.ts b/db/schema/vendorDocu.ts new file mode 100644 index 00000000..2e6ba5a1 --- /dev/null +++ b/db/schema/vendorDocu.ts @@ -0,0 +1,285 @@ +import { pgTable, integer, varchar, timestamp, date ,pgView,uniqueIndex ,jsonb} from "drizzle-orm/pg-core" +import { eq , sql} from "drizzle-orm"; +import { projects } from "./projects"; +import { vendors } from "./vendors"; +import { contracts } from "./contract"; + + +export const documents = pgTable( + "documents", + { + // 주 키 + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // 어느 계약(Contract) 소속인지 + contractId: integer("contract_id") + .notNull() + .references(() => contracts.id, { onDelete: "cascade" }), + + // 예: 문서 번호(유니크 설정 가능) + docNumber: varchar("doc_number", { length: 100 }).notNull(), + // 예: 문서(도서) 제목 + title: varchar("title", { length: 255 }).notNull(), + + // 추가 예시: 발행일, 상태 등 + status: varchar("status", { length: 50 }) + .notNull() + .default("ACTIVE"), + issuedDate: date("issued_date"), + + // 생성/수정 시각 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (table) => { + return { + // contractId + docNumber + status 복합 유니크 + uniqueContractDocStatus: uniqueIndex("unique_contract_doc_status").on( + table.contractId, + table.docNumber, + table.status + ), + } + } +) + +export const issueStages = pgTable( + "issue_stages", + { + // 주 키 + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // 어느 문서 소속인지 + documentId: integer("document_id") + .notNull() + .references(() => documents.id, { onDelete: "cascade" }), + + // 스테이지명 (예: "Issued for Review", "AFC" 등) + stageName: varchar("stage_name", { length: 100 }).notNull(), + + // 계획일, 실제일 + planDate: date("plan_date"), + actualDate: date("actual_date"), + + // 생성/수정 시각 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (table) => { + return { + // document_id + stage_name 조합을 유니크 인덱스로 지정 + uniqueDocumentStage: uniqueIndex("unique_document_stage").on( + table.documentId, + table.stageName + ), + } + } +); + + export const revisions = pgTable( + "revisions", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + issueStageId: integer("issue_stage_id").notNull(), + revision: varchar("revision", { length: 50 }).notNull(), + // 새로운 필드: 업로더 타입 (업체 또는 고객사) + uploaderType: varchar("uploader_type", { length: 20 }).notNull().default("vendor"), + // 선택적: 업로더 ID 또는 이름 + uploaderId: integer("uploader_id"), + uploaderName: varchar("uploader_name", { length: 100 }), + // 선택적: 추가 메타데이터 + comment: varchar("comment", { length: 500 }), + status: varchar("status", { length: 50 }), + approvedDate: date("approved_date"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (table) => { + return { + // "issue_stage_id + revision" 조합을 유니크로 묶음 (유지) + uniqueStageRev: uniqueIndex("unique_stage_rev").on( + table.issueStageId, + table.revision + ), + } + } + ) + export const documentAttachments = pgTable( + "document_attachments", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + revisionId: integer("revision_id") + .notNull() + .references(() => revisions.id, { onDelete: "cascade" }), + 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 vendorDocumentsView = pgTable("vendor_documents", { + // vendorId: integer("vendor_id").notNull(), + // id: integer("id").notNull(), + // docNumber: varchar("doc_number", { length: 100 }).notNull(), + // title: varchar("title", { length: 255 }).notNull(), + + // // 새로 추가된 컬럼들 (contractId, contractNo, contractName, status) + // contractId: integer("contract_id").notNull(), + // contractNo: varchar("contract_no", { length: 100 }).notNull(), + // contractName: varchar("contract_name", { length: 255 }).notNull(), + // status: varchar("status", { length: 50 }).notNull(), + + // createdAt: timestamp("created_at", { withTimezone: true }), + // updatedAt: timestamp("updated_at", { withTimezone: true }), + + // }) + + + export const vendorDocumentsView = pgView("vendor_documents_view", { + // Match the columns in your SELECT statement + id: integer("id").notNull(), + docNumber: varchar("doc_number", { length: 100 }).notNull(), + title: varchar("title", { length: 255 }).notNull(), + status: varchar("status", { length: 50 }).notNull(), + issuedDate: date("issued_date"), + + contractId: integer("contract_id").notNull(), + + latestStageId: integer("latest_stage_id"), // possibly can be null + latestStageName: varchar("latest_stage_name", { length: 100 }), + latestStagePlanDate: date("latest_stage_plan_date"), + latestStageActualDate: date("latest_stage_actual_date"), + + latestRevisionId: integer("latest_revision_id"), + latestRevision: varchar("latest_revision", { length: 50 }), + latestRevisionUploaderType: varchar("latest_revision_uploader_type", { length: 20 }), + latestRevisionUploaderName: varchar("latest_revision_uploader_name", { length: 100 }), + + attachmentCount: integer("attachment_count"), + + createdAt: timestamp("created_at", { withTimezone: true }).notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(), + }).as(sql` + SELECT + d.id, + d.doc_number, + d.title, + d.status, + d.issued_date, + + d.contract_id, + + ( + SELECT id FROM issue_stages + WHERE document_id = d.id + ORDER BY created_at DESC LIMIT 1 + ) AS latest_stage_id, + ( + SELECT stage_name FROM issue_stages + WHERE document_id = d.id + ORDER BY created_at DESC LIMIT 1 + ) AS latest_stage_name, + ( + SELECT plan_date FROM issue_stages + WHERE document_id = d.id + ORDER BY created_at DESC LIMIT 1 + ) AS latest_stage_plan_date, + ( + SELECT actual_date FROM issue_stages + WHERE document_id = d.id + ORDER BY created_at DESC LIMIT 1 + ) AS latest_stage_actual_date, + + ( + SELECT r.id FROM revisions r + JOIN issue_stages i ON r.issue_stage_id = i.id + WHERE i.document_id = d.id + ORDER BY r.created_at DESC LIMIT 1 + ) AS latest_revision_id, + ( + SELECT r.revision FROM revisions r + JOIN issue_stages i ON r.issue_stage_id = i.id + WHERE i.document_id = d.id + ORDER BY r.created_at DESC LIMIT 1 + ) AS latest_revision, + ( + SELECT r.uploader_type FROM revisions r + JOIN issue_stages i ON r.issue_stage_id = i.id + WHERE i.document_id = d.id + ORDER BY r.created_at DESC LIMIT 1 + ) AS latest_revision_uploader_type, + ( + SELECT r.uploader_name FROM revisions r + JOIN issue_stages i ON r.issue_stage_id = i.id + WHERE i.document_id = d.id + ORDER BY r.created_at DESC LIMIT 1 + ) AS latest_revision_uploader_name, + + ( + SELECT COUNT(*) FROM document_attachments a + JOIN revisions r ON a.revision_id = r.id + JOIN issue_stages i ON r.issue_stage_id = i.id + WHERE i.document_id = d.id + ) AS attachment_count, + + d.created_at, + d.updated_at + FROM documents d + JOIN contracts c ON d.contract_id = c.id + `); + + + +// 문서 + 스테이지 리스트 뷰 +export const documentStagesView = pgView("document_stages_view", { + documentId: integer("document_id").notNull(), + docNumber: varchar("doc_number", { length: 100 }).notNull(), + title: varchar("title", { length: 255 }).notNull(), + status: varchar("status", { length: 50 }).notNull(), + issuedDate: date("issued_date"), + contractId: integer("contract_id").notNull(), + stageCount: integer("stage_count").notNull(), + + // 문자열 배열을 받을 것이므로 jsonb + $type<string[]>() + // 스테이지가 없으면 null이 올 수도 있다면 string[] | null + stageList: jsonb("stage_list").$type<string[] | null>(), + + createdAt: timestamp("created_at", { withTimezone: true }).notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(), +}).as(sql` + SELECT + d.id AS document_id, + d.doc_number, + d.title, + d.status, + d.issued_date, + d.contract_id, + + ( + SELECT COUNT(*) + FROM issue_stages + WHERE document_id = d.id + ) AS stage_count, + + COALESCE( + ( + SELECT json_agg(i.stage_name) + FROM issue_stages i + WHERE i.document_id = d.id + ), + '[]' + ) AS stage_list, + + d.created_at, + d.updated_at + FROM documents d +`); + + + export type VendorDocumentsView = typeof vendorDocumentsView.$inferSelect + export type DocumentStagesView = typeof documentStagesView.$inferSelect +
\ No newline at end of file diff --git a/db/schema/vendors.ts b/db/schema/vendors.ts new file mode 100644 index 00000000..b2005537 --- /dev/null +++ b/db/schema/vendors.ts @@ -0,0 +1,118 @@ +// db/schema/vendors.ts +import { pgTable, serial, varchar, text, timestamp, boolean, integer ,pgView} from "drizzle-orm/pg-core"; +import { items } from "./items"; +import { eq} from "drizzle-orm"; + +export const vendors = pgTable("vendors", { + id: serial("id").primaryKey(), + vendorName: varchar("vendor_name", { length: 255 }).notNull(), + vendorCode: varchar("vendor_code", { length: 100 }), + taxId: varchar("tax_id", { length: 100 }).notNull(), + address: text("address"), + country: varchar("country", { length: 100 }), + phone: varchar("phone", { length: 50 }), + email: varchar("email", { length: 255 }), + website: varchar("website", { length: 255 }), + status: varchar("status", { + length: 30, + enum: [ + "PENDING_REVIEW", // 가입 신청 중 (초기 신청) + "IN_REVIEW", // 심사 중 + "REJECTED", // 심사 거부됨 + "IN_PQ", // PQ 진행 중 + "PQ_SUBMITTED", // PQ 제출 + "PQ_FAILED", // PQ 실패 + "APPROVED", // PQ 통과, 승인됨 + "ACTIVE", // 활성 상태 (실제 거래 중) + "INACTIVE", // 비활성 상태 (일시적) + "BLACKLISTED", // 거래 금지 상태 + ] + }) + .notNull() + .default("PENDING_REVIEW"), + + representativeName: varchar("representative_name", { length: 255 }), + representativeBirth: varchar("representative_birth", { length: 20 }), + representativeEmail: varchar("representative_email", { length: 255 }), + representativePhone: varchar("representative_phone", { length: 50 }), + corporateRegistrationNumber: varchar("corporate_registration_number", { + length: 100, + }), + + creditAgency: varchar("credit_agency", { length: 50 }), + creditRating: varchar("credit_rating", { length: 50 }), + cashFlowRating: varchar("cash_flow_rating", { length: 50 }), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const vendorContacts = pgTable("vendor_contacts", { + id: serial("id").primaryKey(), + vendorId: integer("vendor_id").notNull().references(() => vendors.id), + contactName: varchar("contact_name", { length: 255 }).notNull(), + contactPosition: varchar("contact_position", { length: 100 }), + contactEmail: varchar("contact_email", { length: 255 }).notNull(), + contactPhone: varchar("contact_phone", { length: 50 }), + isPrimary: boolean("is_primary").default(false).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + + +export const vendorPossibleItems = pgTable("vendor_possible_items", { + id: serial("id").primaryKey(), + vendorId: integer("vendor_id").notNull().references(() => vendors.id), + // itemId: integer("item_id"), // 별도 item 테이블 연동시 + itemCode: varchar("item_code", { length: 100 }) + .notNull() + .references(() => items.itemCode, { onDelete: "cascade" }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const vendorItemsView = pgView("vendor_items_view").as((qb) => { + return qb + .select({ + // vendorPossibleItems의 "id" -> "vendorItemId" + vendorItemId: vendorPossibleItems.id, + vendorId: vendorPossibleItems.vendorId, + + // items의 "id" -> "itemId" + // itemId: items.id, + itemName: items.itemName, + itemCode: items.itemCode, + description: items.description, + + createdAt: vendorPossibleItems.createdAt, + updatedAt: vendorPossibleItems.updatedAt, + }) + .from(vendorPossibleItems) + .leftJoin(items, eq(vendorPossibleItems.itemCode, items.itemCode)) +}) + +export const vendorAttachments = pgTable("vendor_attachments", { + id: serial("id").primaryKey(), + vendorId: integer("vendor_id").references(() => vendors.id), + fileName: varchar("file_name", { length: 255 }).notNull(), + filePath: varchar("file_path", { length: 1024 }).notNull(), + attachmentType: varchar("attachment_type", { + length: 50, + }).default("GENERAL"), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + + +export type Vendor = typeof vendors.$inferSelect +export type VendorContact = typeof vendorContacts.$inferSelect +export type VendorItem = typeof vendorPossibleItems.$inferSelect +export type VendorAttach = typeof vendorAttachments.$inferSelect + +export type VendorWithAttachments = Vendor & { + hasAttachments?: boolean; + attachmentsList?: VendorAttach[]; +} + +export type VendorItemsView = typeof vendorItemsView.$inferSelect
\ No newline at end of file |
