731 lines
28 KiB
TypeScript
731 lines
28 KiB
TypeScript
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()
|
|
});
|