This commit is contained in:
2026-07-16 09:53:14 +07:00
parent 0fc112a2e8
commit 29cec708a3
1236 changed files with 212848 additions and 0 deletions

730
src/db/schema.ts Normal file
View File

@@ -0,0 +1,730 @@
import {
type AnyPgColumn,
boolean,
doublePrecision,
integer,
jsonb,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex
} from 'drizzle-orm/pg-core';
export const membershipRoleEnum = pgEnum('membership_role', ['owner', 'admin', 'member']);
export const systemRoleEnum = pgEnum('system_role', ['standard', 'super_admin']);
export const trainingTypeEnum = pgEnum('training_type', [
'online',
'onsite',
'internal',
'external'
]);
export const approvalStatusEnum = pgEnum('approval_status', [
'draft',
'pending',
'approved',
'rejected',
'needs_revision'
]);
export const trainingCategoryEnum = pgEnum('training_category', ['K', 'S', 'A']);
export const notificationTypeEnum = pgEnum('notification_type', [
'approved',
'rejected',
'announcement',
'reminder'
]);
export const announcementStatusEnum = pgEnum('announcement_status', [
'draft',
'pending_review',
'approved',
'published',
'returned',
'rejected',
'archived'
]);
export const onlineLessonStatusEnum = pgEnum('online_lesson_status', [
'draft',
'pending_review',
'approved',
'published',
'returned',
'rejected',
'archived'
]);
export const importStatusEnum = pgEnum('import_status', ['processing', 'completed', 'failed']);
export const permissionOverrideEffectEnum = pgEnum('permission_override_effect', ['allow', 'deny']);
export const users = pgTable(
'users',
{
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
username: text('username'),
passwordHash: text('password_hash'),
provider: text('provider').default('credentials').notNull(),
providerUserId: text('provider_user_id'),
phone: text('phone'),
image: text('image'),
systemRole: systemRoleEnum('system_role').default('standard').notNull(),
isActive: boolean('is_active').default(true).notNull(),
employeeCode: text('employee_code'),
employeeId: integer('employee_id').references((): AnyPgColumn => employees.id, {
onDelete: 'set null'
}),
departmentId: integer('department_id'),
positionId: integer('position_id'),
companyName: text('company_name'),
hiredAt: timestamp('hired_at', { withTimezone: true }),
activeOrganizationId: text('active_organization_id'),
lastLoginAt: timestamp('last_login_at', { withTimezone: true }),
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),
usernameIdx: uniqueIndex('users_username_idx').on(table.username),
providerUserIdx: uniqueIndex('users_provider_user_id_idx').on(
table.provider,
table.providerUserId
)
})
);
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(),
isActive: boolean('is_active').default(true).notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
deletedBy: text('deleted_by').references(() => users.id, { onDelete: 'set null' }),
pendingMasterReview: boolean('pending_master_review').default(false).notNull(),
createdBy: text('created_by')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
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()
.references(() => users.id, { onDelete: 'cascade' }),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
role: membershipRoleEnum('role').default('owner').notNull(),
permissions: text('permissions').array().default([]).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
membershipUniqueIdx: uniqueIndex('memberships_user_org_idx').on(
table.userId,
table.organizationId
)
})
);
export const permissionTemplates = pgTable(
'permission_templates',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
code: text('code').notNull(),
name: text('name').notNull(),
description: text('description'),
isSystem: boolean('is_system').default(false).notNull(),
isDefault: boolean('is_default').default(false).notNull(),
isActive: boolean('is_active').default(true).notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
createdBy: text('created_by').references(() => users.id, {
onDelete: 'set null'
}),
updatedBy: text('updated_by').references(() => users.id, {
onDelete: 'set null'
}),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
permissionTemplateCodeUniqueIdx: uniqueIndex('permission_templates_org_code_idx').on(
table.organizationId,
table.code
),
permissionTemplateNameUniqueIdx: uniqueIndex('permission_templates_org_name_idx').on(
table.organizationId,
table.name
)
})
);
export const permissionTemplatePermissions = pgTable(
'permission_template_permissions',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
templateId: integer('template_id')
.notNull()
.references(() => permissionTemplates.id, { onDelete: 'cascade' }),
permission: text('permission').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
permissionTemplatePermissionUniqueIdx: uniqueIndex(
'permission_template_permissions_template_permission_idx'
).on(table.templateId, table.permission)
})
);
export const userPermissionTemplateAssignments = pgTable(
'user_permission_template_assignments',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
templateId: integer('template_id')
.notNull()
.references(() => permissionTemplates.id, { onDelete: 'restrict' }),
isActive: boolean('is_active').default(true).notNull(),
assignedBy: text('assigned_by').references(() => users.id, {
onDelete: 'set null'
}),
assignedAt: timestamp('assigned_at', { withTimezone: true }).defaultNow().notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
userPermissionTemplateAssignmentUniqueIdx: uniqueIndex(
'user_permission_template_assignments_org_user_template_idx'
).on(table.organizationId, table.userId, table.templateId)
})
);
export const userPermissionOverrides = pgTable('user_permission_overrides', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
permission: text('permission').notNull(),
effect: permissionOverrideEffectEnum('effect').notNull(),
reason: text('reason'),
expiresAt: timestamp('expires_at', { withTimezone: true }),
isActive: boolean('is_active').default(true).notNull(),
createdBy: text('created_by').references(() => users.id, {
onDelete: 'set null'
}),
revokedBy: text('revoked_by').references(() => users.id, {
onDelete: 'set null'
}),
revokedAt: timestamp('revoked_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const permissionAuditLogs = pgTable('permission_audit_logs', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id').references(() => organizations.id, {
onDelete: 'set null'
}),
actorUserId: text('actor_user_id').references(() => users.id, {
onDelete: 'set null'
}),
targetUserId: text('target_user_id').references(() => users.id, {
onDelete: 'set null'
}),
templateId: integer('template_id').references(() => permissionTemplates.id, {
onDelete: 'set null'
}),
assignmentId: integer('assignment_id').references(() => userPermissionTemplateAssignments.id, {
onDelete: 'set null'
}),
overrideId: integer('override_id').references(() => userPermissionOverrides.id, {
onDelete: 'set null'
}),
action: text('action').notNull(),
reason: text('reason'),
before: jsonb('before'),
after: jsonb('after'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
});
export const userEmployeeMaps = pgTable(
'user_employee_maps',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
employeeId: integer('employee_id')
.notNull()
.references(() => employees.id, { onDelete: 'cascade' }),
provider: text('provider'),
providerUserId: text('provider_user_id'),
source: text('source').default('manual').notNull(),
isPrimary: boolean('is_primary').default(true).notNull(),
linkedAt: timestamp('linked_at', { withTimezone: true }).defaultNow().notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
userEmployeeMapUniqueIdx: uniqueIndex('user_employee_maps_org_user_employee_idx').on(
table.organizationId,
table.userId,
table.employeeId
)
})
);
export const products = pgTable('products', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
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 departments = pgTable(
'departments',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
code: text('code'),
name: text('name').notNull(),
description: text('description'),
isActive: boolean('is_active').default(true).notNull(),
pendingMasterReview: boolean('pending_master_review').default(false).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
departmentCodeIdx: uniqueIndex('departments_org_code_idx').on(table.organizationId, table.code),
departmentNameIdx: uniqueIndex('departments_org_name_idx').on(table.organizationId, table.name)
})
);
export const positions = pgTable(
'positions',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
code: text('code'),
name: text('name').notNull(),
description: text('description'),
isActive: boolean('is_active').default(true).notNull(),
pendingMasterReview: boolean('pending_master_review').default(false).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
positionCodeIdx: uniqueIndex('positions_org_code_idx').on(table.organizationId, table.code),
positionNameIdx: uniqueIndex('positions_org_name_idx').on(table.organizationId, table.name)
})
);
export const employees = pgTable(
'employees',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
employeeCode: text('employee_code').notNull(),
prefix: text('prefix'),
firstNameTh: text('first_name_th'),
lastNameTh: text('last_name_th'),
firstNameEn: text('first_name_en'),
lastNameEn: text('last_name_en'),
displayName: text('display_name'),
fullName: text('full_name').notNull(),
email: text('email'),
phone: text('phone'),
companyName: text('company_name'),
company: text('company'),
departmentId: integer('department_id').references(() => departments.id, {
onDelete: 'set null'
}),
positionId: integer('position_id').references(() => positions.id, {
onDelete: 'set null'
}),
hiredAt: timestamp('hired_at', { withTimezone: true }),
status: text('status').default('active').notNull(),
isActive: boolean('is_active').default(true).notNull(),
pendingMasterReview: boolean('pending_master_review').default(false).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
employeeCodeIdx: uniqueIndex('employees_org_employee_code_idx').on(
table.organizationId,
table.employeeCode
)
})
);
export const courses = pgTable(
'courses',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
courseCode: text('course_code'),
name: text('name').notNull(),
category: text('category').notNull(),
organizer: text('organizer'),
standardHours: doublePrecision('standard_hours').default(0).notNull(),
certificateValidityMonths: integer('certificate_validity_months'),
isRequired: boolean('is_required').default(false).notNull(),
certificateRequired: boolean('certificate_required').default(true).notNull(),
trainingCost: doublePrecision('training_cost'),
description: text('description'),
isActive: boolean('is_active').default(true).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
courseNameIdx: uniqueIndex('courses_org_name_idx').on(table.organizationId, table.name)
})
);
export const trainingRecords = pgTable('training_records', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
employeeId: integer('employee_id').references(() => employees.id, { onDelete: 'set null' }),
courseId: integer('course_id').references(() => courses.id, { onDelete: 'restrict' }),
courseName: text('course_name').notNull(),
trainingDate: timestamp('training_date', { withTimezone: true }).notNull(),
trainingType: trainingTypeEnum('training_type').notNull(),
submittedMinutes: integer('submitted_minutes'),
hours: doublePrecision('hours').notNull(),
approvedMinutes: integer('approved_minutes'),
approvedHours: doublePrecision('approved_hours'),
category: trainingCategoryEnum('category'),
organizer: text('organizer'),
note: text('note'),
approvalStatus: approvalStatusEnum('approval_status').default('pending').notNull(),
reviewerNote: text('reviewer_note'),
rejectReason: text('reject_reason'),
submittedByUserId: text('submitted_by_user_id').references(() => users.id, {
onDelete: 'set null'
}),
reviewedByUserId: text('reviewed_by_user_id').references(() => users.id, {
onDelete: 'set null'
}),
reviewedBy: text('reviewed_by').references(() => users.id, {
onDelete: 'set null'
}),
reviewedAt: timestamp('reviewed_at', { withTimezone: true }),
createdBy: text('created_by')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const certificates = pgTable('certificates', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
trainingRecordId: integer('training_record_id')
.notNull()
.references(() => trainingRecords.id, { onDelete: 'cascade' }),
fileName: text('file_name').notNull(),
fileType: text('file_type').notNull(),
fileSize: integer('file_size').notNull(),
storageKey: text('storage_key').notNull(),
fileUrl: text('file_url'),
uploadedBy: text('uploaded_by')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const trainingMatrices = pgTable('training_matrices', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
departmentId: integer('department_id').references(() => departments.id, {
onDelete: 'cascade'
}),
positionId: integer('position_id').references(() => positions.id, {
onDelete: 'cascade'
}),
courseId: integer('course_id')
.notNull()
.references(() => courses.id, { onDelete: 'cascade' }),
isRequired: boolean('is_required').default(true).notNull(),
renewalPeriodMonths: integer('renewal_period_months'),
note: text('note'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const trainingPolicies = pgTable(
'training_policies',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
year: integer('year').notNull(),
totalTargetMinutes: integer('total_target_minutes'),
totalHours: doublePrecision('total_hours').default(30).notNull(),
kTargetMinutes: integer('k_target_minutes'),
kHours: doublePrecision('k_hours').default(12).notNull(),
sTargetMinutes: integer('s_target_minutes'),
sHours: doublePrecision('s_hours').default(12).notNull(),
aTargetMinutes: integer('a_target_minutes'),
aHours: doublePrecision('a_hours').default(6).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) => ({
trainingPolicyYearIdx: uniqueIndex('training_policies_org_year_idx').on(
table.organizationId,
table.year
)
})
);
export const employeeTrainingTargets = pgTable(
'employee_training_targets',
{
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
employeeId: integer('employee_id').references(() => employees.id, {
onDelete: 'set null'
}),
year: integer('year').notNull(),
totalTargetMinutes: integer('total_target_minutes'),
totalHours: doublePrecision('total_hours').notNull(),
kTargetMinutes: integer('k_target_minutes'),
kHours: doublePrecision('k_hours').notNull(),
sTargetMinutes: integer('s_target_minutes'),
sHours: doublePrecision('s_hours').notNull(),
aTargetMinutes: integer('a_target_minutes'),
aHours: doublePrecision('a_hours').notNull(),
createdBy: text('created_by')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
updatedBy: text('updated_by').references(() => users.id, {
onDelete: 'set null'
}),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
employeeTrainingTargetUniqueIdx: uniqueIndex('employee_training_targets_org_user_year_idx').on(
table.organizationId,
table.userId,
table.year
),
employeeTrainingTargetEmployeeUniqueIdx: uniqueIndex(
'employee_training_targets_org_employee_year_idx'
).on(table.organizationId, table.employeeId, table.year)
})
);
export const notifications = pgTable('notifications', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
message: text('message').notNull(),
type: notificationTypeEnum('type').notNull(),
referenceType: text('reference_type'),
referenceId: text('reference_id'),
isRead: boolean('is_read').default(false).notNull(),
readAt: timestamp('read_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
});
export const announcements = pgTable('announcements', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
content: text('content').notNull(),
attachmentFileName: text('attachment_file_name'),
attachmentFileType: text('attachment_file_type'),
attachmentFileSize: integer('attachment_file_size'),
attachmentStorageKey: text('attachment_storage_key'),
attachmentFileUrl: text('attachment_file_url'),
startDate: timestamp('start_date', { withTimezone: true }).notNull(),
endDate: timestamp('end_date', { withTimezone: true }),
isPin: boolean('is_pin').default(false).notNull(),
status: announcementStatusEnum('status').default('draft').notNull(),
submittedAt: timestamp('submitted_at', { withTimezone: true }),
submittedBy: text('submitted_by').references(() => users.id, {
onDelete: 'set null'
}),
reviewedAt: timestamp('reviewed_at', { withTimezone: true }),
reviewedBy: text('reviewed_by').references(() => users.id, {
onDelete: 'set null'
}),
approvedAt: timestamp('approved_at', { withTimezone: true }),
approvedBy: text('approved_by').references(() => users.id, {
onDelete: 'set null'
}),
publishedAt: timestamp('published_at', { withTimezone: true }),
publishedBy: text('published_by').references(() => users.id, {
onDelete: 'set null'
}),
reviewComment: text('review_comment'),
createdBy: text('created_by')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
updatedBy: text('updated_by').references(() => users.id, {
onDelete: 'set null'
}),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const onlineLessons = pgTable('online_lessons', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
description: text('description'),
category: text('category'),
videoUrl: text('video_url'),
videoFileName: text('video_file_name'),
videoFileType: text('video_file_type'),
videoFileSize: integer('video_file_size'),
videoStorageKey: text('video_storage_key'),
videoFileUrl: text('video_file_url'),
attachmentFileName: text('attachment_file_name'),
attachmentFileType: text('attachment_file_type'),
attachmentFileSize: integer('attachment_file_size'),
attachmentStorageKey: text('attachment_storage_key'),
attachmentFileUrl: text('attachment_file_url'),
thumbnailFileName: text('thumbnail_file_name'),
thumbnailFileType: text('thumbnail_file_type'),
thumbnailFileSize: integer('thumbnail_file_size'),
thumbnailStorageKey: text('thumbnail_storage_key'),
thumbnailUrl: text('thumbnail_url'),
status: onlineLessonStatusEnum('status').default('draft').notNull(),
submittedAt: timestamp('submitted_at', { withTimezone: true }),
submittedBy: text('submitted_by').references(() => users.id, {
onDelete: 'set null'
}),
reviewedAt: timestamp('reviewed_at', { withTimezone: true }),
reviewedBy: text('reviewed_by').references(() => users.id, {
onDelete: 'set null'
}),
approvedAt: timestamp('approved_at', { withTimezone: true }),
approvedBy: text('approved_by').references(() => users.id, {
onDelete: 'set null'
}),
isPublished: boolean('is_published').default(false).notNull(),
publishedAt: timestamp('published_at', { withTimezone: true }),
publishedBy: text('published_by').references(() => users.id, {
onDelete: 'set null'
}),
reviewComment: text('review_comment'),
createdBy: text('created_by')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
updatedBy: text('updated_by').references(() => users.id, {
onDelete: 'set null'
}),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
export const auditLogs = pgTable('audit_logs', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id').references(() => organizations.id, {
onDelete: 'set null'
}),
userId: text('user_id').references(() => users.id, {
onDelete: 'set null'
}),
moduleName: text('module_name').notNull(),
action: text('action').notNull(),
entityName: text('entity_name'),
entityId: text('entity_id'),
oldValue: jsonb('old_value'),
newValue: jsonb('new_value'),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
});
export const importJobs = pgTable('import_jobs', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
organizationId: text('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
importType: text('import_type').notNull(),
fileName: text('file_name').notNull(),
totalRows: integer('total_rows').default(0).notNull(),
successRows: integer('success_rows').default(0).notNull(),
failedRows: integer('failed_rows').default(0).notNull(),
status: importStatusEnum('status').default('processing').notNull(),
errorMessage: text('error_message'),
createdBy: text('created_by')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
});