807 lines
33 KiB
TypeScript
807 lines
33 KiB
TypeScript
import {
|
|
boolean,
|
|
doublePrecision,
|
|
integer,
|
|
jsonb,
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
uniqueIndex
|
|
} from 'drizzle-orm/pg-core';
|
|
|
|
export const users = pgTable(
|
|
'users',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
email: text('email').notNull(),
|
|
passwordHash: text('password_hash').notNull(),
|
|
systemRole: text('system_role').default('user').notNull(),
|
|
image: text('image'),
|
|
activeOrganizationId: text('active_organization_id'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => ({
|
|
emailIdx: uniqueIndex('users_email_idx').on(table.email)
|
|
})
|
|
);
|
|
|
|
export const organizations = pgTable(
|
|
'organizations',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull(),
|
|
imageUrl: text('image_url'),
|
|
plan: text('plan').default('free').notNull(),
|
|
createdBy: text('created_by').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => ({
|
|
slugIdx: uniqueIndex('organizations_slug_idx').on(table.slug)
|
|
})
|
|
);
|
|
|
|
export const memberships = pgTable('memberships', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull(),
|
|
organizationId: text('organization_id').notNull(),
|
|
role: text('role').default('user').notNull(),
|
|
businessRole: text('business_role').default('sales_support').notNull(),
|
|
permissions: text('permissions').array().default([]).notNull(),
|
|
branchScopeIds: text('branch_scope_ids').array().default([]).notNull(),
|
|
productTypeScopeIds: text('product_type_scope_ids').array().default([]).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
export const crmRoleProfiles = pgTable(
|
|
'crm_role_profiles',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
code: text('code').notNull(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
permissions: text('permissions').array().default([]).notNull(),
|
|
branchScopeMode: text('branch_scope_mode').default('assigned').notNull(),
|
|
productScopeMode: text('product_scope_mode').default('assigned').notNull(),
|
|
ownershipScope: text('ownership_scope').default('own').notNull(),
|
|
approvalAuthority: text('approval_authority').default('none').notNull(),
|
|
isSystem: boolean('is_system').default(false).notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
},
|
|
(table) => ({
|
|
organizationRoleCodeIdx: uniqueIndex('crm_role_profiles_org_code_idx').on(
|
|
table.organizationId,
|
|
table.code
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmUserRoleAssignments = pgTable(
|
|
'crm_user_role_assignments',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
userId: text('user_id').notNull(),
|
|
roleProfileId: text('role_profile_id').notNull(),
|
|
branchScopeMode: text('branch_scope_mode').default('inherit').notNull(),
|
|
branchScopeIds: text('branch_scope_ids').array().default([]).notNull(),
|
|
productTypeScopeMode: text('product_type_scope_mode').default('inherit').notNull(),
|
|
productTypeScopeIds: text('product_type_scope_ids').array().default([]).notNull(),
|
|
isPrimary: boolean('is_primary').default(false).notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
assignedBy: text('assigned_by').notNull(),
|
|
assignedAt: timestamp('assigned_at', { withTimezone: true }).defaultNow().notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
},
|
|
(table) => ({
|
|
organizationUserRoleProfileIdx: uniqueIndex('crm_user_role_assignments_org_user_role_idx').on(
|
|
table.organizationId,
|
|
table.userId,
|
|
table.roleProfileId
|
|
)
|
|
})
|
|
);
|
|
|
|
export const products = pgTable('products', {
|
|
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
|
|
organizationId: text('organization_id').notNull(),
|
|
name: text('name').notNull(),
|
|
category: text('category').notNull(),
|
|
description: text('description').notNull(),
|
|
photoUrl: text('photo_url').notNull(),
|
|
price: doublePrecision('price').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
export const msOptions = pgTable(
|
|
'ms_options',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
category: text('category').notNull(),
|
|
code: text('code').notNull(),
|
|
label: text('label').notNull(),
|
|
value: text('value'),
|
|
parentId: text('parent_id'),
|
|
sortOrder: integer('sort_order').default(0).notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
metadata: jsonb('metadata'),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => ({
|
|
organizationCategoryCodeIdx: uniqueIndex('ms_options_org_category_code_idx').on(
|
|
table.organizationId,
|
|
table.category,
|
|
table.code
|
|
)
|
|
})
|
|
);
|
|
|
|
export const documentSequences = pgTable(
|
|
'document_sequences',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
branchId: text('branch_id').default('').notNull(),
|
|
documentType: text('document_type').notNull(),
|
|
prefix: text('prefix').notNull(),
|
|
period: text('period').notNull(),
|
|
currentNumber: integer('current_number').default(0).notNull(),
|
|
paddingLength: integer('padding_length').default(3).notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => ({
|
|
organizationDocumentPeriodBranchIdx: uniqueIndex(
|
|
'document_sequences_org_doc_period_branch_idx'
|
|
).on(table.organizationId, table.documentType, table.period, table.branchId)
|
|
})
|
|
);
|
|
|
|
export const trAuditLogs = pgTable('tr_audit_logs', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
branchId: text('branch_id'),
|
|
userId: text('user_id').notNull(),
|
|
entityType: text('entity_type').notNull(),
|
|
entityId: text('entity_id').notNull(),
|
|
action: text('action').notNull(),
|
|
beforeData: jsonb('before_data'),
|
|
afterData: jsonb('after_data'),
|
|
requestId: text('request_id'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
export const crmCustomers = pgTable(
|
|
'crm_customers',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
branchId: text('branch_id'),
|
|
code: text('code').notNull(),
|
|
name: text('name').notNull(),
|
|
abbr: text('abbr'),
|
|
taxId: text('tax_id'),
|
|
customerType: text('customer_type').notNull(),
|
|
customerStatus: text('customer_status').notNull(),
|
|
address: text('address'),
|
|
province: text('province'),
|
|
district: text('district'),
|
|
subDistrict: text('sub_district'),
|
|
postalCode: text('postal_code'),
|
|
country: text('country'),
|
|
phone: text('phone'),
|
|
fax: text('fax'),
|
|
email: text('email'),
|
|
website: text('website'),
|
|
leadChannel: text('lead_channel'),
|
|
customerGroup: text('customer_group'),
|
|
customerSubGroup: text('customer_sub_group'),
|
|
ownerUserId: text('owner_user_id'),
|
|
ownerAssignedAt: timestamp('owner_assigned_at', { withTimezone: true }),
|
|
ownerAssignedBy: text('owner_assigned_by'),
|
|
notes: text('notes'),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
},
|
|
(table) => ({
|
|
organizationCodeIdx: uniqueIndex('crm_customers_org_code_idx').on(
|
|
table.organizationId,
|
|
table.code
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmCustomerOwnerHistory = pgTable('crm_customer_owner_history', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
customerId: text('customer_id').notNull(),
|
|
oldOwnerUserId: text('old_owner_user_id'),
|
|
newOwnerUserId: text('new_owner_user_id'),
|
|
changedBy: text('changed_by').notNull(),
|
|
changedAt: timestamp('changed_at', { withTimezone: true }).defaultNow().notNull(),
|
|
remark: text('remark'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmCustomerContacts = pgTable('crm_customer_contacts', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
customerId: text('customer_id').notNull(),
|
|
name: text('name').notNull(),
|
|
position: text('position'),
|
|
department: text('department'),
|
|
phone: text('phone'),
|
|
mobile: text('mobile'),
|
|
email: text('email'),
|
|
isPrimary: boolean('is_primary').default(false).notNull(),
|
|
notes: text('notes'),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
});
|
|
|
|
export const crmLeads = pgTable(
|
|
'crm_leads',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
branchId: text('branch_id'),
|
|
code: text('code').notNull(),
|
|
customerId: text('customer_id'),
|
|
contactId: text('contact_id'),
|
|
description: text('description'),
|
|
projectName: text('project_name'),
|
|
projectLocation: text('project_location'),
|
|
productType: text('product_type'),
|
|
priority: text('priority'),
|
|
estimatedValue: doublePrecision('estimated_value'),
|
|
awarenessId: text('awareness_id'),
|
|
status: text('status').default('new_job').notNull(),
|
|
followupStatus: text('followup_status'),
|
|
lostReason: text('lost_reason'),
|
|
outcome: text('outcome').default('open').notNull(),
|
|
ownerMarketingUserId: text('owner_marketing_user_id'),
|
|
assignedSalesOwnerId: text('assigned_sales_owner_id'),
|
|
assignedAt: timestamp('assigned_at', { withTimezone: true }),
|
|
assignedBy: text('assigned_by'),
|
|
assignmentRemark: text('assignment_remark'),
|
|
createdBy: text('created_by').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
},
|
|
(table) => ({
|
|
organizationCodeIdx: uniqueIndex('crm_leads_org_code_idx').on(table.organizationId, table.code)
|
|
})
|
|
);
|
|
|
|
export const crmContactShares = pgTable(
|
|
'crm_contact_shares',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
contactId: text('contact_id').notNull(),
|
|
sharedToUserId: text('shared_to_user_id').notNull(),
|
|
sharedByUserId: text('shared_by_user_id').notNull(),
|
|
sharedAt: timestamp('shared_at', { withTimezone: true }).defaultNow().notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
remark: text('remark'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
},
|
|
(table) => ({
|
|
contactSharedUserIdx: uniqueIndex('crm_contact_shares_org_contact_shared_user_idx').on(
|
|
table.organizationId,
|
|
table.contactId,
|
|
table.sharedToUserId
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmEnquiries = pgTable(
|
|
'crm_enquiries',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
branchId: text('branch_id'),
|
|
leadId: text('lead_id'),
|
|
code: text('code').notNull(),
|
|
customerId: text('customer_id').notNull(),
|
|
contactId: text('contact_id'),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
requirement: text('requirement'),
|
|
projectName: text('project_name'),
|
|
projectLocation: text('project_location'),
|
|
productType: text('product_type').notNull(),
|
|
status: text('status').notNull(),
|
|
priority: text('priority').notNull(),
|
|
leadChannel: text('lead_channel'),
|
|
estimatedValue: doublePrecision('estimated_value'),
|
|
chancePercent: integer('chance_percent'),
|
|
expectedCloseDate: timestamp('expected_close_date', { withTimezone: true }),
|
|
competitor: text('competitor'),
|
|
source: text('source'),
|
|
notes: text('notes'),
|
|
isHotProject: boolean('is_hot_project').default(false).notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
pipelineStage: text('pipeline_stage').default('lead').notNull(),
|
|
closedWonAt: timestamp('closed_won_at', { withTimezone: true }),
|
|
closedLostAt: timestamp('closed_lost_at', { withTimezone: true }),
|
|
closedByUserId: text('closed_by_user_id'),
|
|
poNumber: text('po_number'),
|
|
poDate: timestamp('po_date', { withTimezone: true }),
|
|
poAmount: doublePrecision('po_amount'),
|
|
poCurrency: text('po_currency'),
|
|
lostReason: text('lost_reason'),
|
|
lostCompetitor: text('lost_competitor'),
|
|
lostRemark: text('lost_remark'),
|
|
assignedToUserId: text('assigned_to_user_id'),
|
|
assignedAt: timestamp('assigned_at', { withTimezone: true }),
|
|
assignedBy: text('assigned_by'),
|
|
assignmentRemark: text('assignment_remark'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
},
|
|
(table) => ({
|
|
organizationCodeIdx: uniqueIndex('crm_enquiries_org_code_idx').on(
|
|
table.organizationId,
|
|
table.code
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmEnquiryFollowups = pgTable('crm_enquiry_followups', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
enquiryId: text('enquiry_id').notNull(),
|
|
followupDate: timestamp('followup_date', { withTimezone: true }).notNull(),
|
|
followupType: text('followup_type').notNull(),
|
|
contactId: text('contact_id'),
|
|
outcome: text('outcome'),
|
|
notes: text('notes'),
|
|
nextFollowupDate: timestamp('next_followup_date', { withTimezone: true }),
|
|
nextAction: text('next_action'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
});
|
|
|
|
export const crmEnquiryCustomers = pgTable('crm_enquiry_customers', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
enquiryId: text('enquiry_id').notNull(),
|
|
customerId: text('customer_id').notNull(),
|
|
role: text('role').notNull(),
|
|
remark: text('remark'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmEnquiryAttachments = pgTable('crm_enquiry_attachments', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
enquiryId: text('enquiry_id').notNull(),
|
|
category: text('category').notNull(),
|
|
fileName: text('file_name').notNull(),
|
|
originalFileName: text('original_file_name').notNull(),
|
|
storageProvider: text('storage_provider').notNull(),
|
|
storageKey: text('storage_key').notNull(),
|
|
fileSize: integer('file_size'),
|
|
fileType: text('file_type'),
|
|
description: text('description'),
|
|
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull(),
|
|
uploadedBy: text('uploaded_by').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmReportDefinitions = pgTable(
|
|
'crm_report_definitions',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
code: text('code').notNull(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
category: text('category').notNull(),
|
|
isSystem: boolean('is_system').default(true).notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => ({
|
|
organizationCodeIdx: uniqueIndex('crm_report_definitions_org_code_idx').on(
|
|
table.organizationId,
|
|
table.code
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmQuotations = pgTable(
|
|
'crm_quotations',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
branchId: text('branch_id'),
|
|
code: text('code').notNull(),
|
|
enquiryId: text('enquiry_id'),
|
|
customerId: text('customer_id').notNull(),
|
|
contactId: text('contact_id'),
|
|
quotationDate: timestamp('quotation_date', { withTimezone: true }).notNull(),
|
|
validUntil: timestamp('valid_until', { withTimezone: true }),
|
|
quotationType: text('quotation_type').notNull(),
|
|
projectName: text('project_name'),
|
|
projectLocation: text('project_location'),
|
|
attention: text('attention'),
|
|
reference: text('reference'),
|
|
notes: text('notes'),
|
|
status: text('status').notNull(),
|
|
revision: integer('revision').default(0).notNull(),
|
|
parentQuotationId: text('parent_quotation_id'),
|
|
revisionRemark: text('revision_remark'),
|
|
currency: text('currency').notNull(),
|
|
exchangeRate: doublePrecision('exchange_rate').default(1).notNull(),
|
|
subtotal: doublePrecision('subtotal').default(0).notNull(),
|
|
discount: doublePrecision('discount').default(0).notNull(),
|
|
discountType: text('discount_type'),
|
|
taxRate: doublePrecision('tax_rate').default(0).notNull(),
|
|
taxAmount: doublePrecision('tax_amount').default(0).notNull(),
|
|
totalAmount: doublePrecision('total_amount').default(0).notNull(),
|
|
chancePercent: integer('chance_percent'),
|
|
isHotProject: boolean('is_hot_project').default(false).notNull(),
|
|
competitor: text('competitor'),
|
|
salesmanId: text('salesman_id'),
|
|
isSent: boolean('is_sent').default(false).notNull(),
|
|
sentAt: timestamp('sent_at', { withTimezone: true }),
|
|
sentVia: text('sent_via'),
|
|
approvedAt: timestamp('approved_at', { withTimezone: true }),
|
|
approvedArtifactId: text('approved_artifact_id'),
|
|
approvedPdfUrl: text('approved_pdf_url'),
|
|
approvedSnapshot: jsonb('approved_snapshot'),
|
|
approvedTemplateVersionId: text('approved_template_version_id'),
|
|
acceptedAt: timestamp('accepted_at', { withTimezone: true }),
|
|
rejectedAt: timestamp('rejected_at', { withTimezone: true }),
|
|
rejectionReason: text('rejection_reason'),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
},
|
|
(table) => ({
|
|
organizationCodeIdx: uniqueIndex('crm_quotations_org_code_idx').on(
|
|
table.organizationId,
|
|
table.code
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmQuotationItems = pgTable('crm_quotation_items', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
quotationId: text('quotation_id').notNull(),
|
|
itemNumber: integer('item_number').notNull(),
|
|
productType: text('product_type').notNull(),
|
|
description: text('description').notNull(),
|
|
quantity: doublePrecision('quantity').default(0).notNull(),
|
|
unit: text('unit'),
|
|
unitPrice: doublePrecision('unit_price').default(0).notNull(),
|
|
discount: doublePrecision('discount').default(0).notNull(),
|
|
discountType: text('discount_type'),
|
|
taxRate: doublePrecision('tax_rate').default(0).notNull(),
|
|
totalPrice: doublePrecision('total_price').default(0).notNull(),
|
|
notes: text('notes'),
|
|
sortOrder: integer('sort_order').default(0).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmQuotationCustomers = pgTable('crm_quotation_customers', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
quotationId: text('quotation_id').notNull(),
|
|
customerId: text('customer_id').notNull(),
|
|
role: text('role').notNull(),
|
|
isPrimary: boolean('is_primary').default(false).notNull(),
|
|
remark: text('remark'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmQuotationTopics = pgTable('crm_quotation_topics', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
quotationId: text('quotation_id').notNull(),
|
|
topicType: text('topic_type').notNull(),
|
|
title: text('title').notNull(),
|
|
sortOrder: integer('sort_order').default(0).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmQuotationTopicItems = pgTable('crm_quotation_topic_items', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
topicId: text('topic_id').notNull(),
|
|
content: text('content').notNull(),
|
|
sortOrder: integer('sort_order').default(0).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmQuotationFollowups = pgTable('crm_quotation_followups', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
quotationId: text('quotation_id').notNull(),
|
|
followupDate: timestamp('followup_date', { withTimezone: true }).notNull(),
|
|
followupType: text('followup_type').notNull(),
|
|
contactId: text('contact_id'),
|
|
outcome: text('outcome'),
|
|
notes: text('notes'),
|
|
nextFollowupDate: timestamp('next_followup_date', { withTimezone: true }),
|
|
nextAction: text('next_action'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
});
|
|
|
|
export const crmQuotationAttachments = pgTable('crm_quotation_attachments', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
quotationId: text('quotation_id').notNull(),
|
|
fileName: text('file_name').notNull(),
|
|
originalFileName: text('original_file_name').notNull(),
|
|
filePath: text('file_path').notNull(),
|
|
fileSize: integer('file_size'),
|
|
fileType: text('file_type'),
|
|
description: text('description'),
|
|
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull(),
|
|
uploadedBy: text('uploaded_by').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmDocumentArtifacts = pgTable('crm_document_artifacts', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
entityType: text('entity_type').notNull(),
|
|
entityId: text('entity_id').notNull(),
|
|
documentType: text('document_type').notNull(),
|
|
artifactType: text('artifact_type').notNull(),
|
|
templateVersionId: text('template_version_id'),
|
|
storageProvider: text('storage_provider').notNull(),
|
|
storageKey: text('storage_key').notNull(),
|
|
fileName: text('file_name').notNull(),
|
|
contentType: text('content_type').notNull(),
|
|
fileSize: integer('file_size'),
|
|
checksum: text('checksum'),
|
|
status: text('status').default('active').notNull(),
|
|
generatedBy: text('generated_by').notNull(),
|
|
generatedAt: timestamp('generated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
lockedAt: timestamp('locked_at', { withTimezone: true }),
|
|
lockedBy: text('locked_by'),
|
|
voidedAt: timestamp('voided_at', { withTimezone: true }),
|
|
voidedBy: text('voided_by'),
|
|
voidReason: text('void_reason'),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmApprovalWorkflows = pgTable(
|
|
'crm_approval_workflows',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
code: text('code').notNull(),
|
|
name: text('name').notNull(),
|
|
entityType: text('entity_type').notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
},
|
|
(table) => ({
|
|
organizationCodeIdx: uniqueIndex('crm_approval_workflows_org_code_idx').on(
|
|
table.organizationId,
|
|
table.code
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmApprovalSteps = pgTable(
|
|
'crm_approval_steps',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
workflowId: text('workflow_id').notNull(),
|
|
stepNumber: integer('step_number').notNull(),
|
|
roleCode: text('role_code').notNull(),
|
|
roleName: text('role_name').notNull(),
|
|
isRequired: boolean('is_required').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
},
|
|
(table) => ({
|
|
workflowStepIdx: uniqueIndex('crm_approval_steps_workflow_step_idx').on(
|
|
table.workflowId,
|
|
table.stepNumber
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmApprovalRequests = pgTable('crm_approval_requests', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
workflowId: text('workflow_id').notNull(),
|
|
entityType: text('entity_type').notNull(),
|
|
entityId: text('entity_id').notNull(),
|
|
currentStep: integer('current_step').default(1).notNull(),
|
|
status: text('status').notNull(),
|
|
requestedBy: text('requested_by').notNull(),
|
|
requestedAt: timestamp('requested_at', { withTimezone: true }).defaultNow().notNull(),
|
|
completedAt: timestamp('completed_at', { withTimezone: true }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmApprovalActions = pgTable('crm_approval_actions', {
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
approvalRequestId: text('approval_request_id').notNull(),
|
|
stepNumber: integer('step_number').notNull(),
|
|
action: text('action').notNull(),
|
|
remark: text('remark'),
|
|
actedBy: text('acted_by').notNull(),
|
|
actedAt: timestamp('acted_at', { withTimezone: true }).defaultNow().notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
});
|
|
|
|
export const crmDocumentTemplates = pgTable(
|
|
'crm_document_templates',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
documentType: text('document_type').notNull(),
|
|
productType: text('product_type').notNull(),
|
|
fileType: text('file_type').notNull(),
|
|
templateName: text('template_name').notNull(),
|
|
description: text('description'),
|
|
isDefault: boolean('is_default').default(false).notNull(),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull(),
|
|
updatedBy: text('updated_by').notNull()
|
|
},
|
|
(table) => ({
|
|
organizationTemplateIdx: uniqueIndex('crm_document_templates_org_doc_product_file_name_idx').on(
|
|
table.organizationId,
|
|
table.documentType,
|
|
table.productType,
|
|
table.fileType,
|
|
table.templateName
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmDocumentTemplateVersions = pgTable(
|
|
'crm_document_template_versions',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
templateId: text('template_id').notNull(),
|
|
version: text('version').notNull(),
|
|
filePath: text('file_path'),
|
|
schemaJson: jsonb('schema_json').notNull(),
|
|
previewImageUrl: text('preview_image_url'),
|
|
isActive: boolean('is_active').default(true).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
createdBy: text('created_by').notNull()
|
|
},
|
|
(table) => ({
|
|
templateVersionIdx: uniqueIndex('crm_document_template_versions_template_version_idx').on(
|
|
table.templateId,
|
|
table.version
|
|
)
|
|
})
|
|
);
|
|
|
|
export const crmDocumentTemplateMappings = pgTable(
|
|
'crm_document_template_mappings',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
templateVersionId: text('template_version_id').notNull(),
|
|
placeholderKey: text('placeholder_key').notNull(),
|
|
sourcePath: text('source_path').notNull(),
|
|
dataType: text('data_type').notNull(),
|
|
sheetName: text('sheet_name'),
|
|
defaultValue: text('default_value'),
|
|
formatMask: text('format_mask'),
|
|
sortOrder: integer('sort_order').default(0).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
},
|
|
(table) => ({
|
|
templatePlaceholderIdx: uniqueIndex(
|
|
'crm_document_template_mappings_version_placeholder_idx'
|
|
).on(table.templateVersionId, table.placeholderKey)
|
|
})
|
|
);
|
|
|
|
export const crmDocumentTemplateTableColumns = pgTable(
|
|
'crm_document_template_table_columns',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id').notNull(),
|
|
mappingId: text('mapping_id').notNull(),
|
|
columnName: text('column_name').notNull(),
|
|
sourceField: text('source_field').notNull(),
|
|
columnLetter: text('column_letter'),
|
|
sortOrder: integer('sort_order').default(0).notNull(),
|
|
formatMask: text('format_mask'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
|
},
|
|
(table) => ({
|
|
mappingColumnIdx: uniqueIndex('crm_document_template_table_columns_mapping_name_idx').on(
|
|
table.mappingId,
|
|
table.columnName
|
|
)
|
|
})
|
|
);
|