Files
alla-allaos-fullstack/src/db/schema.ts
phaichayon 9b05743b1e task-d.7.8
2026-07-03 10:19:03 +07:00

1191 lines
49 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 setupRuns = pgTable('setup_runs', {
id: text('id').primaryKey(),
status: text('status').default('not_started').notNull(),
mode: text('mode').default('production').notNull(),
targetOrganizationId: text('target_organization_id'),
startedBy: text('started_by').notNull(),
completedBy: text('completed_by'),
startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(),
completedAt: timestamp('completed_at', { withTimezone: true }),
lastError: text('last_error'),
metadata: jsonb('metadata').$type<Record<string, unknown> | null>(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const setupRunSteps = pgTable(
'setup_run_steps',
{
id: text('id').primaryKey(),
setupRunId: text('setup_run_id').notNull(),
stepKey: text('step_key').notNull(),
status: text('status').default('not_started').notNull(),
inputHash: text('input_hash'),
outputJson: jsonb('output_json').$type<Record<string, unknown> | null>(),
errorsJson: jsonb('errors_json').$type<unknown[] | null>(),
warningsJson: jsonb('warnings_json').$type<unknown[] | null>(),
startedAt: timestamp('started_at', { withTimezone: true }),
completedAt: timestamp('completed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
setupRunStepIdx: uniqueIndex('setup_run_steps_run_step_idx').on(
table.setupRunId,
table.stepKey
)
})
);
export const seedManifests = pgTable('seed_manifests', {
id: text('id').primaryKey(),
setupRunId: text('setup_run_id'),
organizationId: text('organization_id'),
name: text('name').notNull(),
version: text('version').notNull(),
type: text('type').notNull(),
checksum: text('checksum').notNull(),
status: text('status').default('pending').notNull(),
source: text('source').notNull(),
appliedBy: text('applied_by'),
appliedAt: timestamp('applied_at', { withTimezone: true }),
reportJson: jsonb('report_json').$type<Record<string, unknown> | null>(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const seedManifestItems = pgTable(
'seed_manifest_items',
{
id: text('id').primaryKey(),
seedManifestId: text('seed_manifest_id').notNull(),
itemKey: text('item_key').notNull(),
sourceFile: text('source_file').notNull(),
checksum: text('checksum').notNull(),
status: text('status').default('pending').notNull(),
importedCount: integer('imported_count').default(0).notNull(),
updatedCount: integer('updated_count').default(0).notNull(),
skippedCount: integer('skipped_count').default(0).notNull(),
errorCount: integer('error_count').default(0).notNull(),
warningCount: integer('warning_count').default(0).notNull(),
reportJson: jsonb('report_json').$type<Record<string, unknown> | null>(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
seedManifestItemIdx: uniqueIndex('seed_manifest_items_manifest_item_idx').on(
table.seedManifestId,
table.itemKey
)
})
);
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(),
productType: text('product_type').default('all').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(),
format: text('format').default('{prefix}{period}-{running}').notNull(),
resetPolicy: text('reset_policy').default('period').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) => ({
organizationBranchProductDocumentPeriodIdx: uniqueIndex(
'document_sequences_org_branch_product_doc_period_idx'
).on(
table.organizationId,
table.branchId,
table.productType,
table.documentType,
table.period
)
})
);
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 appNotificationEvents = pgTable(
'app_notification_events',
{
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
eventType: text('event_type').notNull(),
entityType: text('entity_type').notNull(),
entityId: text('entity_id').notNull(),
actorUserId: text('actor_user_id'),
payload: jsonb('payload'),
dedupeKey: text('dedupe_key'),
status: text('status').default('pending').notNull(),
lastError: text('last_error'),
publishedAt: timestamp('published_at', { withTimezone: true }).defaultNow().notNull(),
processedAt: timestamp('processed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
organizationDedupeIdx: uniqueIndex('app_notification_events_org_dedupe_idx').on(
table.organizationId,
table.dedupeKey
)
})
);
export const appNotifications = pgTable(
'app_notifications',
{
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
recipientUserId: text('recipient_user_id').notNull(),
eventId: text('event_id').notNull(),
title: text('title').notNull(),
body: text('body').notNull(),
linkUrl: text('link_url'),
severity: text('severity').default('info').notNull(),
status: text('status').default('unread').notNull(),
readAt: timestamp('read_at', { withTimezone: true }),
archivedAt: timestamp('archived_at', { withTimezone: true }),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
recipientEventIdx: uniqueIndex('app_notifications_recipient_event_idx').on(
table.recipientUserId,
table.eventId
)
})
);
export const appNotificationTemplates = pgTable(
'app_notification_templates',
{
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
eventType: text('event_type').notNull(),
code: text('code').default('').notNull(),
name: text('name').default('').notNull(),
channel: text('channel').notNull(),
subjectTemplate: text('subject_template'),
titleTemplate: text('title_template').notNull(),
bodyTemplate: text('body_template').notNull(),
bodyFormat: text('body_format').default('plain').notNull(),
linkTemplate: text('link_template'),
language: text('language').default('th').notNull(),
variables: jsonb('variables'),
version: integer('version').default(1).notNull(),
isActive: boolean('is_active').default(true).notNull(),
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 }),
createdBy: text('created_by').notNull(),
updatedBy: text('updated_by').notNull()
},
(table) => ({
organizationEventChannelIdx: uniqueIndex('app_notification_templates_org_event_channel_idx').on(
table.organizationId,
table.eventType,
table.channel
)
})
);
export const appNotificationDispatches = pgTable(
'app_notification_dispatches',
{
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
eventId: text('event_id').notNull(),
templateId: text('template_id'),
entityType: text('entity_type').notNull(),
entityId: text('entity_id').notNull(),
channel: text('channel').notNull(),
providerKey: text('provider_key').notNull(),
status: text('status').default('pending').notNull(),
recipientTo: jsonb('recipient_to').notNull(),
recipientCc: jsonb('recipient_cc'),
recipientBcc: jsonb('recipient_bcc'),
replyTo: text('reply_to'),
subject: text('subject').notNull(),
bodyText: text('body_text').notNull(),
bodyHtml: text('body_html'),
attachments: jsonb('attachments'),
recipientUserId: text('recipient_user_id'),
recipientAddress: text('recipient_address'),
requestedBy: text('requested_by').notNull(),
metadata: jsonb('metadata'),
lastError: text('last_error'),
retryCount: integer('retry_count').default(0).notNull(),
maxRetryCount: integer('max_retry_count').default(3).notNull(),
lastAttemptAt: timestamp('last_attempt_at', { withTimezone: true }),
sentAt: timestamp('sent_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
organizationEventChannelRecipientIdx: uniqueIndex(
'app_notification_dispatches_org_event_channel_recipient_idx'
).on(table.organizationId, table.eventId, table.channel, table.recipientAddress)
})
);
export const appNotificationDeliveries = pgTable('app_notification_deliveries', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
notificationId: text('notification_id').notNull(),
channel: text('channel').notNull(),
recipientAddress: text('recipient_address'),
status: text('status').default('pending').notNull(),
attemptCount: integer('attempt_count').default(0).notNull(),
lastError: text('last_error'),
sentAt: timestamp('sent_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_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'),
leadChannel: text('lead_channel'),
productType: text('product_type'),
priority: text('priority'),
estimatedValue: doublePrecision('estimated_value'),
awarenessId: text('awareness_id'),
status: text('status').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 crmOpportunities = pgTable(
'crm_opportunities',
{
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(),
outcomeStatus: text('outcome_status').default('open').notNull(),
priority: text('priority').notNull(),
leadChannel: text('lead_channel'),
estimatedValue: doublePrecision('estimated_value'),
chancePercent: integer('chance_percent'),
expectedCloseDate: timestamp('expected_close_date', { withTimezone: true }),
projectCloseDate: timestamp('project_close_date', { withTimezone: true }),
deliveryDate: timestamp('delivery_date', { withTimezone: true }),
competitor: text('competitor'),
source: text('source'),
notes: text('notes'),
isHotProject: boolean('is_hot_project').default(false).notNull(),
hotProjectAutoSuggested: boolean('hot_project_auto_suggested').default(false).notNull(),
hotProjectManuallyOverridden: boolean('hot_project_manually_overridden')
.default(false)
.notNull(),
isActive: boolean('is_active').default(true).notNull(),
pipelineStage: text('pipeline_stage').default('lead').notNull(),
closedAt: timestamp('closed_at', { withTimezone: true }),
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'),
lostDetail: text('lost_detail'),
lostCompetitor: text('lost_competitor'),
lostRemark: text('lost_remark'),
cancelReason: text('cancel_reason'),
noQuotationReason: text('no_quotation_reason'),
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_opportunities_org_code_idx').on(
table.organizationId,
table.code
)
})
);
export const crmOpportunityFollowups = pgTable('crm_opportunity_followups', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
opportunityId: text('opportunity_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 crmOpportunityCustomers = pgTable('crm_opportunity_customers', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
opportunityId: text('opportunity_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 crmOpportunityAttachments = pgTable('crm_opportunity_attachments', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
opportunityId: text('opportunity_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(),
opportunityId: text('opportunity_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'),
projectCloseDate: timestamp('project_close_date', { withTimezone: true }),
deliveryDate: timestamp('delivery_date', { withTimezone: true }),
isHotProject: boolean('is_hot_project').default(false).notNull(),
hotProjectAutoSuggested: boolean('hot_project_auto_suggested').default(false).notNull(),
hotProjectManuallyOverridden: boolean('hot_project_manually_overridden')
.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(),
description: text('description'),
isSystem: boolean('is_system').default(false).notNull(),
isActive: boolean('is_active').default(true).notNull(),
createdBy: text('created_by').notNull(),
updatedBy: text('updated_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_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(),
approvalMode: text('approval_mode').default('sequential').notNull(),
isRequired: boolean('is_required').default(true).notNull(),
slaHours: integer('sla_hours').default(24).notNull(),
calendarMode: text('calendar_mode').default('calendar_days').notNull(),
timeoutAction: text('timeout_action').default('none').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 crmApprovalMatrices = pgTable('crm_approval_matrices', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
entityType: text('entity_type').notNull(),
workflowId: text('workflow_id').notNull(),
branchId: text('branch_id'),
productType: text('product_type'),
minAmount: doublePrecision('min_amount'),
maxAmount: doublePrecision('max_amount'),
currency: text('currency'),
priority: integer('priority').default(100).notNull(),
isDefault: boolean('is_default').default(false).notNull(),
isActive: boolean('is_active').default(true).notNull(),
description: text('description'),
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 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(),
currentStepStartedAt: timestamp('current_step_started_at', { withTimezone: true })
.defaultNow()
.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 crmApprovalReminderPolicies = pgTable(
'crm_approval_reminder_policies',
{
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
workflowId: text('workflow_id').notNull(),
stepNumber: integer('step_number').notNull(),
slaHours: integer('sla_hours').default(24).notNull(),
reminderOffsetsHours: integer('reminder_offsets_hours').array().default([]).notNull(),
calendarMode: text('calendar_mode').default('calendar_days').notNull(),
timeoutAction: text('timeout_action').default('none').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) => ({
workflowStepIdx: uniqueIndex('crm_approval_reminder_policies_workflow_step_idx').on(
table.workflowId,
table.stepNumber
)
})
);
export const crmApprovalEscalationPolicies = pgTable('crm_approval_escalation_policies', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
workflowId: text('workflow_id').notNull(),
stepNumber: integer('step_number').notNull(),
triggerAfterHours: integer('trigger_after_hours').notNull(),
targetType: text('target_type').notNull(),
targetValue: text('target_value'),
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 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(),
metadataJson: jsonb('metadata_json'),
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 crmDocumentLibraries = pgTable(
'crm_document_libraries',
{
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
code: text('code').notNull(),
name: text('name').notNull(),
documentType: text('document_type').notNull(),
description: text('description'),
brand: text('brand').notNull(),
language: text('language').notNull(),
productType: text('product_type').notNull(),
status: text('status').default('active').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) => ({
organizationCodeIdx: uniqueIndex('crm_document_libraries_org_code_idx').on(
table.organizationId,
table.code
)
})
);
export const crmDocumentLibraryVersions = pgTable(
'crm_document_library_versions',
{
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
libraryId: text('library_id').notNull(),
version: text('version').notNull(),
fileName: text('file_name').notNull(),
storageProvider: text('storage_provider').notNull(),
storageKey: text('storage_key').notNull(),
mimeType: text('mime_type').notNull(),
fileSize: integer('file_size').notNull(),
checksum: text('checksum').notNull(),
pageCount: integer('page_count').notNull(),
status: text('status').default('draft').notNull(),
isActive: boolean('is_active').default(false).notNull(),
publishedAt: timestamp('published_at', { withTimezone: true }),
publishedBy: text('published_by'),
archivedAt: timestamp('archived_at', { withTimezone: true }),
archivedBy: text('archived_by'),
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) => ({
libraryVersionIdx: uniqueIndex('crm_document_library_versions_library_version_idx').on(
table.libraryId,
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
)
})
);