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 | 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 | null>(), errorsJson: jsonb('errors_json').$type(), warningsJson: jsonb('warnings_json').$type(), 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 | 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 | 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 ) }) );