2026-02-04 00:17:04 +05:00
const { Pool , Client } = require ( 'pg' ) ;
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
const bcrypt = require ( 'bcryptjs' ) ;
const DB _NAME = 'mkd_control_center' ;
// Учётная запись администратора портала по умолчанию (создаётся в новых/пустых БД)
const DEFAULT _ADMIN _EMPLOYEE _ID = 'e-admin' ;
const DEFAULT _ADMIN _EMPLOYEE _NAME = 'iiEasy' ;
const DEFAULT _ADMIN _LOGIN = 'its' ;
const DEFAULT _ADMIN _PASSWORD = 'iiEasy348ax@' ;
const DEFAULT _ADMIN _ROLE = 'DIRECTOR' ;
2026-02-10 11:55:20 +05:00
// Демо-пользователь с теми же правами, что и its (DIRECTOR)
const DEMO _EMPLOYEE _ID = 'e-demo' ;
const DEMO _EMPLOYEE _NAME = 'Demo' ;
const DEMO _LOGIN = 'demo' ;
const DEMO _PASSWORD = 'demo123' ;
const DEMO _ROLE = 'DIRECTOR' ;
2026-02-04 00:17:04 +05:00
/ * *
* Инициализация базы данных PostgreSQL
* Создает БД при первом запуске и проверяет целостность при последующих запусках
* /
async function initializeDatabase ( ) {
const DATABASE _URL = process . env . DATABASE _URL ;
if ( ! DATABASE _URL ) {
console . warn ( '[dbInit] DATABASE_URL не задана, пропускаем инициализацию БД' ) ;
return { success : false , reason : 'DATABASE_URL not set' } ;
}
try {
// Парсим URL для получения параметров подключения
const url = new URL ( DATABASE _URL ) ;
const dbName = url . pathname . slice ( 1 ) || DB _NAME ;
// Подключаемся к postgres БД для создания новой БД
const adminUrl = DATABASE _URL . replace ( ` / ${ dbName } ` , '/postgres' ) ;
const adminClient = new Client ( { connectionString : adminUrl } ) ;
await adminClient . connect ( ) ;
console . log ( '[dbInit] Подключено к PostgreSQL' ) ;
// Проверяем существование БД
const dbCheckResult = await adminClient . query (
'SELECT 1 FROM pg_database WHERE datname = $1' ,
[ dbName ]
) ;
if ( dbCheckResult . rows . length === 0 ) {
console . log ( ` [dbInit] База данных ${ dbName } не найдена, создаем... ` ) ;
await adminClient . query ( ` CREATE DATABASE ${ dbName } WITH ENCODING 'UTF8' ` ) ;
console . log ( ` [dbInit] База данных ${ dbName } успешно создана ` ) ;
} else {
console . log ( ` [dbInit] База данных ${ dbName } уже существует ` ) ;
}
await adminClient . end ( ) ;
// Подключаемся к нашей БД для выполнения миграций
const pool = new Pool ( { connectionString : DATABASE _URL } ) ;
const client = await pool . connect ( ) ;
try {
// Проверяем целостность БД
const integrityCheck = await checkDatabaseIntegrity ( client ) ;
if ( ! integrityCheck . isInitialized ) {
console . log ( '[dbInit] База данных не инициализирована, выполняем миграции...' ) ;
// Удаляем проблемную таблицу meeting_bookings, если она существует с ошибкой
try {
await client . query ( 'DROP TABLE IF EXISTS meeting_bookings CASCADE' ) ;
console . log ( '[dbInit] Удалена проблемная таблица meeting_bookings для пересоздания' ) ;
} catch ( error ) {
// Игнорируем ошибки
}
await runMigrations ( client ) ;
console . log ( '[dbInit] Миграции выполнены успешно' ) ;
await applyColumnMigrations ( client ) ;
// Применяем миграцию добавления статуса 'deferred' в enum
await applyDeferredStatusMigration ( client ) ;
// Применяем миграцию для отслеживания производительности
await applyPerformanceTrackingMigration ( client ) ;
// Применяем миграцию модуля обучения
await applyTrainingModuleMigration ( client ) ;
// Применяем миграцию таблиц офиса
await applyOfficeTablesMigration ( client ) ;
// Применяем миграцию PR/NPS
await applyPRNPSMigration ( client ) ;
await applyNPSSurveysMigration ( client ) ;
await applyNPSBuildingStatsMigration ( client ) ;
await applyResidentReportDataMigration ( client ) ;
// Применяем миграцию настроек компании
await applyCompanySettingsMigration ( client ) ;
// Применяем миграцию досудебной работы
await applyPreTrialWorkMigration ( client ) ;
// Применяем миграцию проверок контрагентов
await applyCounterpartyChecksMigration ( client ) ;
await applyPortalUsersRoleMigration ( client ) ;
await applyPortalUsersAuthMigration ( client ) ;
await applyUserProfileFieldsMigration ( client ) ;
await applyPermissionTemplatesMigration ( client ) ;
await applyIntegrationSettingsMigration ( client ) ;
await applySecurityModuleMigration ( client ) ;
// Применяем миграцию модуля развития
await applyDevelopmentModuleMigration ( client ) ;
// Применяем миграцию автоматизации воронки
await applyPipelineAutomationMigration ( client ) ;
// Применяем миграцию новых этапов воронки (9 пунктов)
await applyDevelopmentPipelineNewStagesMigration ( client ) ;
// Добавляем этап «Анализ» в воронку
await applyDevelopmentPipelineAddAnalysisMigration ( client ) ;
// Аудиты: статусы, индекс сложности, данные осмотра
await applyDevelopmentAuditsStatusAndInspectionMigration ( client ) ;
// О С С : повестка и голоса по пунктам
await applyDevelopmentOSSAgendaMigration ( client ) ;
// Применяем миграцию юридического отдела (договоры, судебные дела)
await applyLegalModuleMigration ( client ) ;
// Применяем миграцию добавления inventory в districts
await applyDistrictsInventoryMigration ( client ) ;
// Таблицы ИИ-чата
await applyAIChatMigration ( client ) ;
// Таблица уведомлений (колокольчик)
await applyNotificationsMigration ( client ) ;
await applyEmployeeResponsibilityMigration ( client ) ;
} else {
console . log ( '[dbInit] База данных уже инициализирована' ) ;
// Применяем миграции даже для уже инициализированной БД (в т.ч. positions и др.)
console . log ( '[dbInit] Применяем дополнительные миграции...' ) ;
await applyColumnMigrations ( client ) ;
await applyDeferredStatusMigration ( client ) ;
await applyPerformanceTrackingMigration ( client ) ;
await applyDomaMappingsMigration ( client ) ;
await applyPortalUsersRoleMigration ( client ) ;
await applyPortalUsersAuthMigration ( client ) ;
await applyUserProfileFieldsMigration ( client ) ;
await applyPermissionTemplatesMigration ( client ) ;
await applyIntegrationSettingsMigration ( client ) ;
await applySecurityModuleMigration ( client ) ;
await applyNotificationsMigration ( client ) ;
await applyBuildingPersonalAccountsMigration ( client ) ;
if ( ! integrityCheck . isHealthy ) {
console . warn ( '[dbInit] Обнаружены проблемы с целостностью БД:' ) ;
integrityCheck . issues . forEach ( issue => console . warn ( ` - ${ issue } ` ) ) ;
// Пытаемся исправить проблемы, выполнив миграции для недостающих таблиц
console . log ( '[dbInit] Пытаемся исправить проблемы, выполняя миграции...' ) ;
try {
await runMigrations ( client ) ;
console . log ( '[dbInit] Миграции для исправления проблем выполнены' ) ;
await applyColumnMigrations ( client ) ;
// Применяем миграцию таблиц офиса
await applyOfficeTablesMigration ( client ) ;
// Применяем миграцию настроек компании
await applyCompanySettingsMigration ( client ) ;
// Применяем миграцию досудебной работы
await applyPreTrialWorkMigration ( client ) ;
// Применяем миграцию модуля развития
await applyDevelopmentModuleMigration ( client ) ;
// Применяем миграцию автоматизации воронки
await applyPipelineAutomationMigration ( client ) ;
// Применяем миграцию новых этапов воронки (9 пунктов)
await applyDevelopmentPipelineNewStagesMigration ( client ) ;
// Добавляем этап «Анализ» в воронку
await applyDevelopmentPipelineAddAnalysisMigration ( client ) ;
await applyDevelopmentAuditsStatusAndInspectionMigration ( client ) ;
await applyDevelopmentOSSAgendaMigration ( client ) ;
// Повторно проверяем целостность
const recheck = await checkDatabaseIntegrity ( client ) ;
if ( recheck . isHealthy ) {
console . log ( '[dbInit] Проблемы исправлены успешно' ) ;
} else {
console . warn ( '[dbInit] Некоторые проблемы остались:' ) ;
recheck . issues . forEach ( issue => console . warn ( ` - ${ issue } ` ) ) ;
}
} catch ( migrationError ) {
console . error ( '[dbInit] Ошибка при попытке исправить проблемы:' , migrationError . message ) ;
}
} else {
console . log ( '[dbInit] Проверка целостности пройдена успешно' ) ;
// Оптимизация: не выполнять все миграции при каждом старте — проверяем версию схемы
let skipMigrations = false ;
try {
await client . query ( 'CREATE TABLE IF NOT EXISTS schema_version (version INT PRIMARY KEY)' ) ;
const verResult = await client . query ( 'SELECT version FROM schema_version LIMIT 1' ) ;
if ( verResult . rows . length > 0 && Number ( verResult . rows [ 0 ] . version ) >= 1 ) {
skipMigrations = true ;
console . log ( '[dbInit] Схема актуальна (schema_version=1), пропуск повторных миграций' ) ;
}
} catch ( e ) {
// таблица или запрос не сработали — выполняем миграции
}
if ( ! skipMigrations ) {
await applyColumnMigrations ( client ) ;
await applyCandidateEventsMigration ( client ) ;
await applyTrainingModuleMigration ( client ) ;
await applyOfficeTablesMigration ( client ) ;
await applyPRNPSMigration ( client ) ;
await applyNPSSurveysMigration ( client ) ;
await applyNPSBuildingStatsMigration ( client ) ;
await applyResidentReportDataMigration ( client ) ;
await applyCompanySettingsMigration ( client ) ;
await applyPreTrialWorkMigration ( client ) ;
await applyDistrictsInventoryMigration ( client ) ;
await applyCounterpartyChecksMigration ( client ) ;
await applyDevelopmentModuleMigration ( client ) ;
await applyPipelineAutomationMigration ( client ) ;
await applyDevelopmentPipelineNewStagesMigration ( client ) ;
await applyDevelopmentPipelineAddAnalysisMigration ( client ) ;
await applyDevelopmentAuditsStatusAndInspectionMigration ( client ) ;
await applyDevelopmentOSSAgendaMigration ( client ) ;
await applyLegalModuleMigration ( client ) ;
await applyDistrictsInventoryMigration ( client ) ;
await applyAIChatMigration ( client ) ;
await applyNotificationsMigration ( client ) ;
await applyEmployeeResponsibilityMigration ( client ) ;
await client . query ( 'INSERT INTO schema_version (version) VALUES (1) ON CONFLICT (version) DO UPDATE SET version = 1' ) ;
}
}
}
// Администратор портала по умолчанию (логин its) — создаётся, если ещё нет
await applyDefaultAdminSeed ( client ) ;
2026-02-10 11:55:20 +05:00
// Демо-пользователь (demo / demo123) с правами DIRECTOR — создаётся при каждом старте, если ещё нет
await applyDemoUserSeed ( client ) ;
2026-02-04 00:17:04 +05:00
return {
success : true ,
initialized : ! integrityCheck . isInitialized ,
healthy : integrityCheck . isHealthy ,
issues : integrityCheck . issues
} ;
} finally {
client . release ( ) ;
await pool . end ( ) ;
}
} catch ( error ) {
console . error ( '[dbInit] Ошибка при инициализации БД:' , error . message ) ;
return { success : false , error : error . message } ;
}
}
/ * *
* Проверка целостности базы данных
* /
async function checkDatabaseIntegrity ( client ) {
const issues = [ ] ;
let isInitialized = false ;
let isHealthy = true ;
// Список обязательных таблиц
const requiredTables = [
'districts' ,
'buildings' ,
'applications' ,
'financial_reports' ,
'financial_report_mappings' ,
'building_financial_data' ,
'office_equipment' ,
'office_repair_requests' ,
'office_supply_requests' ,
'office_orders' ,
'office_order_items' ,
'office_order_quotes' ,
'office_inventory' ,
'office_documents' ,
'knowledge_base_categories' ,
'knowledge_base_articles' ,
'reviews' ,
'incidents' ,
'resident_reports' ,
'nps_surveys' ,
'nps_responses' ,
'work_photos' ,
'parsing_settings' ,
'meeting_rooms' ,
'meetings' ,
'meeting_bookings' ,
'employees' ,
'employee_messenger_logins' ,
'employee_passport_data' ,
'employee_labor_books' ,
'labor_book_entries' ,
'employee_certificates' ,
'employee_other_documents' ,
'employee_accounting_data' ,
'employee_contracts' ,
'employee_vacations' ,
'employee_sick_leaves' ,
'employee_terminations' ,
'employee_absences' ,
'portal_users' ,
'vacancies' ,
'candidates'
] ;
// Список обязательных типов
const requiredTypes = [
'doma_application_status' ,
'office_equipment_type' ,
'repair_request_status' ,
'employee_status' ,
'messenger_type' ,
'certificate_status' ,
'termination_status' ,
'vacancy_status' ,
'candidate_stage'
] ;
try {
// Проверяем наличие таблиц
for ( const tableName of requiredTables ) {
const result = await client . query (
` SELECT EXISTS (
SELECT FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = $1
) ` ,
[ tableName ]
) ;
if ( ! result . rows [ 0 ] . exists ) {
issues . push ( ` Таблица ${ tableName } отсутствует ` ) ;
isHealthy = false ;
}
}
// Проверяем наличие типов
for ( const typeName of requiredTypes ) {
const result = await client . query (
` SELECT EXISTS (
SELECT FROM pg _type
WHERE typname = $1
) ` ,
[ typeName ]
) ;
if ( ! result . rows [ 0 ] . exists ) {
issues . push ( ` Тип ${ typeName } отсутствует ` ) ;
isHealthy = false ;
}
}
// Если все таблицы на месте, считаем БД инициализированной
if ( issues . length === 0 ) {
isInitialized = true ;
}
// Проверяем структуру таблиц (наличие обязательных колонок)
if ( isInitialized ) {
const tableChecks = [
{ table : 'districts' , columns : [ 'id' , 'name' , 'manager_name' ] } ,
{ table : 'buildings' , columns : [ 'id' , 'data' ] } ,
{ table : 'applications' , columns : [ 'id' , 'number' , 'status' , 'description' , 'address' , 'apartment' , 'client_name' , 'created_at' , 'deadline_at' ] } ,
{ table : 'employees' , columns : [ 'id' , 'name' , 'position' , 'phone' , 'status' , 'salary' , 'assigned_district_id' , 'manager_id' , 'birth_date' , 'photo_url' , 'registration_date' , 'created_at' , 'updated_at' ] }
] ;
for ( const check of tableChecks ) {
const result = await client . query (
` SELECT column_name
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = $1 ` ,
[ check . table ]
) ;
const existingColumns = result . rows . map ( r => r . column _name ) ;
const missingColumns = check . columns . filter ( col => ! existingColumns . includes ( col ) ) ;
if ( missingColumns . length > 0 ) {
issues . push ( ` Таблица ${ check . table } : отсутствуют колонки ${ missingColumns . join ( ', ' ) } ` ) ;
isHealthy = false ;
}
}
}
} catch ( error ) {
issues . push ( ` Ошибка при проверке целостности: ${ error . message } ` ) ;
isHealthy = false ;
}
return {
isInitialized ,
isHealthy ,
issues
} ;
}
/ * *
* Выполнение миграций из schema . sql
* /
async function runMigrations ( client ) {
const schemaPath = path . join ( _ _dirname , 'schema.sql' ) ;
if ( ! fs . existsSync ( schemaPath ) ) {
throw new Error ( ` Файл schema.sql не найден: ${ schemaPath } ` ) ;
}
const schemaSQL = fs . readFileSync ( schemaPath , 'utf8' ) ;
// Убираем комментарии CREATE DATABASE и строки с комментариями
let cleanedSQL = schemaSQL
. split ( '\n' )
. filter ( line => {
const trimmed = line . trim ( ) ;
// Убираем строки с CREATE DATABASE и комментарии
return ! trimmed . startsWith ( '--' ) &&
! trimmed . toLowerCase ( ) . includes ( 'create database' ) &&
trimmed . length > 0 ;
} )
. join ( '\n' ) ;
// Выполняем SQL в транзакции
await client . query ( 'BEGIN' ) ;
try {
// Выполняем весь SQL файл целиком
await client . query ( cleanedSQL ) ;
await client . query ( 'COMMIT' ) ;
console . log ( '[dbInit] В с е миграции выполнены успешно' ) ;
} catch ( error ) {
await client . query ( 'ROLLBACK' ) ;
// Если ошибка связана с зависимостями или "уже существует", используем fallback
if ( error . message . includes ( 'already exists' ) ||
error . message . includes ( 'уже существует' ) ||
error . message . includes ( 'duplicate key' ) ||
error . message . includes ( 'does not exist' ) ||
error . code === '42P01' ) { // 42P01 = relation does not exist
console . log ( '[dbInit] Обнаружена ошибка зависимостей или существующих объектов, выполняю по частям...' ) ;
// Пробуем выполнить по частям
await executeStatementsIndividually ( client , cleanedSQL ) ;
} else {
console . error ( '[dbInit] Ошибка при выполнении миграций:' , error . message ) ;
console . error ( '[dbInit] Детали ошибки:' , error ) ;
throw error ;
}
}
}
/ * *
* Выполнение SQL по частям ( fallback метод )
* /
async function executeStatementsIndividually ( client , sql ) {
// Более умный парсинг - учитываем строковые литералы и dollar-quoted строки
const statements = [ ] ;
let currentStatement = '' ;
let inString = false ;
let stringChar = '' ;
let inDollarQuote = false ;
let dollarTag = '' ;
for ( let i = 0 ; i < sql . length ; i ++ ) {
const char = sql [ i ] ;
// Обработка dollar-quoted строк ($$ или $tag$)
if ( char === '$' && ! inString ) {
// Проверяем, начинается ли dollar-quoted строка
if ( ! inDollarQuote ) {
// Ищем начало: $ или $tag$
let tagEnd = i + 1 ;
while ( tagEnd < sql . length && sql [ tagEnd ] !== '$' ) {
tagEnd ++ ;
}
if ( tagEnd < sql . length ) {
dollarTag = sql . substring ( i , tagEnd + 1 ) ; // Включая о б а $
inDollarQuote = true ;
currentStatement += char ;
continue ;
}
} else {
// Проверяем, заканчивается ли dollar-quoted строка
// Проверяем, совпадает ли последовательность с открывающим тегом
let matches = true ;
for ( let k = 0 ; k < dollarTag . length && ( i + k ) < sql . length ; k ++ ) {
if ( sql [ i + k ] !== dollarTag [ k ] ) {
matches = false ;
break ;
}
}
if ( matches && ( i + dollarTag . length - 1 ) < sql . length ) {
// Найден закрывающий тег - добавляем все символы тега
for ( let k = 0 ; k < dollarTag . length ; k ++ ) {
currentStatement += sql [ i + k ] ;
}
// Пропускаем все символы тега (включая текущий $)
i += dollarTag . length - 1 ; // -1 потому что цикл увеличит i еще на 1
inDollarQuote = false ;
dollarTag = '' ;
continue ; // Пропускаем добавление char в конце цикла
}
}
}
// Обработка обычных строковых литералов (только если не в dollar-quote)
if ( ! inDollarQuote && ( char === "'" || char === '"' ) && ( i === 0 || sql [ i - 1 ] !== '\\' ) ) {
if ( ! inString ) {
inString = true ;
stringChar = char ;
} else if ( char === stringChar ) {
inString = false ;
stringChar = '' ;
}
}
currentStatement += char ;
// Если точка с запятой вне строки и вне dollar-quote - конец команды
if ( char === ';' && ! inString && ! inDollarQuote ) {
const trimmed = currentStatement . trim ( ) ;
if ( trimmed . length > 0 && ! trimmed . startsWith ( '--' ) ) {
statements . push ( trimmed ) ;
}
currentStatement = '' ;
}
}
// Добавляем последнюю команду, если она есть
if ( currentStatement . trim ( ) . length > 0 ) {
statements . push ( currentStatement . trim ( ) ) ;
}
console . log ( ` [dbInit] Выполняю ${ statements . length } SQL команд по отдельности... ` ) ;
// Выполняем каждую команду (без транзакции, т.к. она уже откатилась)
let successCount = 0 ;
let errorCount = 0 ;
for ( let i = 0 ; i < statements . length ; i ++ ) {
const statement = statements [ i ] ;
if ( statement . trim ( ) . length > 0 && ! statement . startsWith ( '--' ) ) {
try {
await client . query ( statement ) ;
successCount ++ ;
if ( ( i + 1 ) % 10 === 0 ) {
console . log ( ` [dbInit] Выполнено ${ i + 1 } из ${ statements . length } команд (успешно: ${ successCount } , ошибок: ${ errorCount } )... ` ) ;
}
} catch ( error ) {
errorCount ++ ;
// Игнорируем ошибки "уже существует" и "does not exist" для зависимостей
if ( ! error . message . includes ( 'already exists' ) &&
! error . message . includes ( 'уже существует' ) &&
! error . message . includes ( 'duplicate key' ) &&
! error . message . includes ( 'duplicate' ) &&
! ( error . code === '42P01' && error . message . includes ( 'does not exist' ) ) ) {
console . warn ( ` [dbInit] Ошибка при выполнении команды ${ i + 1 } : ${ error . message } ` ) ;
console . warn ( ` [dbInit] Код ошибки: ${ error . code } ` ) ;
console . warn ( ` [dbInit] Проблемная команда (первые 200 символов): ${ statement . substring ( 0 , 200 ) } ... ` ) ;
}
}
}
}
console . log ( ` [dbInit] В с е команды обработаны. Успешно: ${ successCount } , ошибок (проигнорировано): ${ errorCount } ` ) ;
}
/ * *
* Лицевые счета домов — отдельная таблица ( применяется и при уже инициализированной БД )
* /
async function applyBuildingPersonalAccountsMigration ( client ) {
try {
const check = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'building_personal_accounts' `
) ;
if ( check . rows . length === 0 ) {
const bpaPath = path . join ( _ _dirname , 'migrations' , 'create_building_personal_accounts.sql' ) ;
if ( fs . existsSync ( bpaPath ) ) {
const sql = fs . readFileSync ( bpaPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_building_personal_accounts.sql применена успешно' ) ;
}
}
} catch ( err ) {
console . warn ( '[dbInit] Ошибка миграции building_personal_accounts:' , err . message ) ;
}
}
/ * *
* Применение миграций для добавления отсутствующих колонок
* /
async function applyColumnMigrations ( client ) {
try {
// Проверяем наличие колонки manager_id в таблице employees
const checkResult = await client . query (
` SELECT column_name
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = 'employees'
AND column _name = 'manager_id' `
) ;
if ( checkResult . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем отсутствующую колонку manager_id в таблицу employees...' ) ;
await client . query ( `
ALTER TABLE employees
ADD COLUMN IF NOT EXISTS manager _id VARCHAR ( 50 ) REFERENCES employees ( id ) ON DELETE SET NULL
` );
console . log ( '[dbInit] Колонка manager_id успешно добавлена' ) ;
// Создаем индекс, если е г о нет
await client . query ( `
CREATE INDEX IF NOT EXISTS idx _employees _manager ON employees ( manager _id )
` );
}
// Проверяем другие важные колонки employees
const employeeColumns = [
{ name : 'assigned_district_id' , type : 'VARCHAR(50) REFERENCES districts(id) ON DELETE SET NULL' } ,
{ name : 'birth_date' , type : 'DATE' } ,
{ name : 'photo_url' , type : 'TEXT' } ,
{ name : 'registration_date' , type : 'DATE' }
] ;
for ( const col of employeeColumns ) {
const colCheck = await client . query (
` SELECT column_name
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = 'employees'
AND column _name = $1 ` ,
[ col . name ]
) ;
if ( colCheck . rows . length === 0 ) {
console . log ( ` [dbInit] Добавляем отсутствующую колонку ${ col . name } в таблицу employees... ` ) ;
await client . query ( `
ALTER TABLE employees
ADD COLUMN IF NOT EXISTS $ { col . name } $ { col . type }
` );
console . log ( ` [dbInit] Колонка ${ col . name } успешно добавлена ` ) ;
}
}
// Предварительная дата выхода для больничного (employee_sick_leaves)
const sickLeaveColCheck = await client . query (
` SELECT column_name FROM information_schema.columns
WHERE table _schema = 'public' AND table _name = 'employee_sick_leaves' AND column _name = 'expected_return_date' `
) ;
if ( sickLeaveColCheck . rows . length === 0 ) {
await client . query ( `
ALTER TABLE employee _sick _leaves ADD COLUMN IF NOT EXISTS expected _return _date DATE
` );
console . log ( '[dbInit] Колонка expected_return_date добавлена в employee_sick_leaves' ) ;
}
// Отпуска: добавляем статус 'rejected' в CHECK (для отклонения отпуска руководителем)
try {
const vacConstraintCheck = await client . query (
` SELECT conname FROM pg_constraint
WHERE conrelid = 'public.employee_vacations' : : regclass AND contype = 'c' `
) ;
for ( const row of vacConstraintCheck . rows || [ ] ) {
const defRes = await client . query (
` SELECT pg_get_constraintdef(oid) AS def FROM pg_constraint WHERE conname = $ 1 ` ,
[ row . conname ]
) ;
const def = ( defRes . rows [ 0 ] && defRes . rows [ 0 ] . def ) || '' ;
if ( def . includes ( 'status' ) && ! def . includes ( "'rejected'" ) ) {
const conname = String ( row . conname ) . replace ( /"/g , '""' ) ;
await client . query ( ` ALTER TABLE employee_vacations DROP CONSTRAINT IF EXISTS " ${ conname } " ` ) ;
await client . query ( `
ALTER TABLE employee _vacations ADD CONSTRAINT employee _vacations _status _check
CHECK ( status IN ( 'planned' , 'approved' , 'active' , 'completed' , 'canceled' , 'rejected' ) )
` );
console . log ( '[dbInit] В employee_vacations добавлен статус rejected' ) ;
break ;
}
}
} catch ( e ) {
console . warn ( '[dbInit] Миграция статуса rejected для отпусков:' , e . message ) ;
}
// Проверяем и создаем таблицу candidate_events, если её нет
await applyCandidateEventsMigration ( client ) ;
// Проверяем и создаем таблицы PR мероприятий (pr_events, pr_event_assignees, pr_event_photos)
const prEventsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pr_events' `
) ;
if ( prEventsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицы pr_events, pr_event_assignees, pr_event_photos...' ) ;
const prEventsPath = path . join ( _ _dirname , 'migrations' , 'create_pr_events.sql' ) ;
if ( fs . existsSync ( prEventsPath ) ) {
const prEventsSQL = fs . readFileSync ( prEventsPath , 'utf8' ) ;
await client . query ( prEventsSQL ) ;
console . log ( '[dbInit] Миграция create_pr_events.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_pr_events.sql не найден' ) ;
}
}
// Добавить колонки места проведения для жителей (участок / дома) в pr_events (если таблица уже есть или только создана)
const prEventsExistsNow = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pr_events' `
) ;
if ( prEventsExistsNow . rows . length > 0 ) {
const addLocationPlacePath = path . join ( _ _dirname , 'migrations' , 'add_pr_events_location_place.sql' ) ;
if ( fs . existsSync ( addLocationPlacePath ) ) {
try {
const addLocationPlaceSQL = fs . readFileSync ( addLocationPlacePath , 'utf8' ) ;
await client . query ( addLocationPlaceSQL ) ;
console . log ( '[dbInit] Миграция add_pr_events_location_place.sql применена успешно' ) ;
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении add_pr_events_location_place.sql:' , err . message ) ;
}
}
}
}
// Таблица новостей компании (сводка + офис)
const companyNewsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'company_news' `
) ;
if ( companyNewsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицу company_news...' ) ;
const companyNewsPath = path . join ( _ _dirname , 'migrations' , 'create_company_news.sql' ) ;
if ( fs . existsSync ( companyNewsPath ) ) {
const companyNewsSQL = fs . readFileSync ( companyNewsPath , 'utf8' ) ;
await client . query ( companyNewsSQL ) ;
console . log ( '[dbInit] Миграция create_company_news.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_company_news.sql не найден' ) ;
}
}
// PR SMM: каналы и снимки подписчиков
const prSmmChannelsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pr_smm_channels' `
) ;
if ( prSmmChannelsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицы pr_smm_channels, pr_smm_subscriber_snapshots...' ) ;
const prSmmPath = path . join ( _ _dirname , 'migrations' , 'create_pr_smm_channels.sql' ) ;
if ( fs . existsSync ( prSmmPath ) ) {
const prSmmSQL = fs . readFileSync ( prSmmPath , 'utf8' ) ;
await client . query ( prSmmSQL ) ;
console . log ( '[dbInit] Миграция create_pr_smm_channels.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_pr_smm_channels.sql не найден' ) ;
}
}
// PR Привлечение: pr_attraction_actions
const prAttractionCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pr_attraction_actions' `
) ;
if ( prAttractionCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицу pr_attraction_actions...' ) ;
const prAttractionPath = path . join ( _ _dirname , 'migrations' , 'create_pr_attraction_actions.sql' ) ;
if ( fs . existsSync ( prAttractionPath ) ) {
const prAttractionSQL = fs . readFileSync ( prAttractionPath , 'utf8' ) ;
await client . query ( prAttractionSQL ) ;
console . log ( '[dbInit] Миграция create_pr_attraction_actions.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_pr_attraction_actions.sql не найден' ) ;
}
}
// PR Темы постов: pr_post_topics
const prPostTopicsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pr_post_topics' `
) ;
if ( prPostTopicsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицу pr_post_topics...' ) ;
const prPostTopicsPath = path . join ( _ _dirname , 'migrations' , 'create_pr_post_topics.sql' ) ;
if ( fs . existsSync ( prPostTopicsPath ) ) {
const prPostTopicsSQL = fs . readFileSync ( prPostTopicsPath , 'utf8' ) ;
await client . query ( prPostTopicsSQL ) ;
console . log ( '[dbInit] Миграция create_pr_post_topics.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_pr_post_topics.sql не найден' ) ;
}
} else {
// Добавить scheduled_date если таблица уже существует
const scheduledDateCheck = await client . query (
` SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pr_post_topics' AND column_name = 'scheduled_date' `
) ;
if ( scheduledDateCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем scheduled_date в pr_post_topics...' ) ;
const addScheduledDatePath = path . join ( _ _dirname , 'migrations' , 'add_scheduled_date_to_post_topics.sql' ) ;
if ( fs . existsSync ( addScheduledDatePath ) ) {
const addScheduledDateSQL = fs . readFileSync ( addScheduledDatePath , 'utf8' ) ;
await client . query ( addScheduledDateSQL ) ;
console . log ( '[dbInit] Миграция add_scheduled_date_to_post_topics.sql применена успешно' ) ;
}
}
}
// PR Отложенные посты: pr_scheduled_posts
const prScheduledPostsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pr_scheduled_posts' `
) ;
if ( prScheduledPostsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицу pr_scheduled_posts...' ) ;
const prScheduledPostsPath = path . join ( _ _dirname , 'migrations' , 'create_pr_scheduled_posts.sql' ) ;
if ( fs . existsSync ( prScheduledPostsPath ) ) {
const prScheduledPostsSQL = fs . readFileSync ( prScheduledPostsPath , 'utf8' ) ;
await client . query ( prScheduledPostsSQL ) ;
console . log ( '[dbInit] Миграция create_pr_scheduled_posts.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_pr_scheduled_posts.sql не найден' ) ;
}
} else {
// Добавить image_url если таблица уже существует
const imageUrlCheck = await client . query (
` SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pr_scheduled_posts' AND column_name = 'image_url' `
) ;
if ( imageUrlCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем image_url в pr_scheduled_posts...' ) ;
const addImagePath = path . join ( _ _dirname , 'migrations' , 'add_image_to_scheduled_posts.sql' ) ;
if ( fs . existsSync ( addImagePath ) ) {
const addImageSQL = fs . readFileSync ( addImagePath , 'utf8' ) ;
await client . query ( addImageSQL ) ;
console . log ( '[dbInit] Миграция add_image_to_scheduled_posts.sql применена успешно' ) ;
}
}
}
// Проверяем и создаем таблицы модуля обучения, если их нет
await applyTrainingModuleMigration ( client ) ;
// Проверяем и создаем таблицы офиса, если их нет
await applyOfficeTablesMigration ( client ) ;
// Проверяем наличие колонки report_type в таблице financial_reports
const reportTypeCheck = await client . query (
` SELECT column_name
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = 'financial_reports'
AND column _name = 'report_type' `
) ;
if ( reportTypeCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем отсутствующую колонку report_type в таблицу financial_reports...' ) ;
const migrationPath = path . join ( _ _dirname , 'migrations' , 'add_report_type.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция add_report_type.sql применена успешно' ) ;
} else {
// Если файл миграции не найден, применяем SQL напрямую
await client . query ( `
ALTER TABLE financial _reports
ADD COLUMN IF NOT EXISTS report _type VARCHAR ( 50 ) DEFAULT 'other'
CHECK ( report _type IN ( 'debtors' , 'balance_sheet' , 'other' ) )
` );
await client . query ( `
CREATE INDEX IF NOT EXISTS idx _financial _reports _type ON financial _reports ( report _type )
` );
console . log ( '[dbInit] Колонка report_type успешно добавлена' ) ;
}
}
// Миграции О С В 76: при каждом старте расширяем report_type и при необходимости создаём таблицы
try {
// Снимаем старый CHECK по report_type (имя может отличаться; схема — public)
await client . query ( ` ALTER TABLE financial_reports DROP CONSTRAINT IF EXISTS financial_reports_report_type_check ` ) ;
const { rows : checkConstraints } = await client . query ( `
SELECT c . conname
FROM pg _constraint c
JOIN pg _class t ON t . oid = c . conrelid
JOIN pg _namespace n ON n . oid = t . relnamespace
WHERE n . nspname = 'public' AND t . relname = 'financial_reports' AND c . contype = 'c'
AND ( pg _get _constraintdef ( c . oid ) LIKE '%report_type%' OR c . conname LIKE '%report_type%' )
` );
for ( const row of checkConstraints ) {
const name = row . conname . replace ( /"/g , '""' ) ;
await client . query ( ` ALTER TABLE financial_reports DROP CONSTRAINT IF EXISTS " ${ name } " ` ) ;
}
await client . query ( `
ALTER TABLE financial _reports ADD CONSTRAINT financial _reports _report _type _check
CHECK ( report _type IN ( 'debtors' , 'balance_sheet' , 'balance_sheet_76' , 'other' ) ) ;
` );
await client . query ( `
COMMENT ON COLUMN financial _reports . report _type IS 'Тип отчета: debtors, balance_sheet (О С В 20), balance_sheet_76 (О С В 76 — лицевые счета), other' ;
` );
console . log ( '[dbInit] Миграция add_report_type_balance_sheet_76.sql применена успешно' ) ;
} catch ( err ) {
const msg = ( err . message || '' ) . toLowerCase ( ) ;
if ( ! msg . includes ( 'already exists' ) && ! msg . includes ( 'does not exist' ) && ! msg . includes ( 'already has constraint' ) ) {
console . warn ( '[dbInit] Ошибка при применении add_report_type_balance_sheet_76.sql:' , err . message ) ;
}
}
// Таблицы О С В 76: report_76_rows, building_personal_account_mappings — создаём при следующем старте, если ещё нет
const report76TablesCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'report_76_rows' `
) ;
if ( report76TablesCheck . rows . length === 0 ) {
const report76TablesPath = path . join ( _ _dirname , 'migrations' , 'create_report_76_tables.sql' ) ;
if ( fs . existsSync ( report76TablesPath ) ) {
try {
const report76TablesSQL = fs . readFileSync ( report76TablesPath , 'utf8' ) ;
await client . query ( report76TablesSQL ) ;
console . log ( '[dbInit] Миграция create_report_76_tables.sql применена успешно' ) ;
} catch ( err ) {
console . warn ( '[dbInit] Ошибка при применении create_report_76_tables.sql:' , err . message ) ;
}
}
}
// Применяем миграцию обновления UNIQUE constraint для building_financial_data
// Проверяем текущий constraint
const constraintCheck = await client . query (
` SELECT constraint_name
FROM information _schema . table _constraints
WHERE table _schema = 'public'
AND table _name = 'building_financial_data'
AND constraint _type = 'UNIQUE'
AND constraint _name LIKE '%building_id%period%' `
) ;
const uniqueConstraintPath = path . join ( _ _dirname , 'migrations' , 'update_building_financial_data_unique.sql' ) ;
if ( fs . existsSync ( uniqueConstraintPath ) ) {
try {
const uniqueConstraintSQL = fs . readFileSync ( uniqueConstraintPath , 'utf8' ) ;
await client . query ( uniqueConstraintSQL ) ;
console . log ( '[dbInit] Миграция update_building_financial_data_unique.sql применена успешно' ) ;
} catch ( err ) {
// Игнорируем ошибки, если constraint уже изменен или не существует
if ( ! err . message . includes ( 'does not exist' ) && ! err . message . includes ( 'already exists' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции update_building_financial_data_unique.sql:' , err . message ) ;
}
}
}
// Применяем миграцию счетов на оплату
const paymentInvoicesCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'payment_invoices' `
) ;
if ( paymentInvoicesCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицу payment_invoices...' ) ;
const paymentInvoicesPath = path . join ( _ _dirname , 'migrations' , 'create_payment_invoices.sql' ) ;
if ( fs . existsSync ( paymentInvoicesPath ) ) {
const paymentInvoicesSQL = fs . readFileSync ( paymentInvoicesPath , 'utf8' ) ;
await client . query ( paymentInvoicesSQL ) ;
console . log ( '[dbInit] Миграция create_payment_invoices.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_payment_invoices.sql не найден' ) ;
}
}
// Типовые документы HR (для печати/шаблонов в разделе Кадры)
const hrTemplatesCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'hr_template_documents' `
) ;
if ( hrTemplatesCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицу hr_template_documents...' ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS hr _template _documents (
id BIGSERIAL PRIMARY KEY ,
name TEXT NOT NULL ,
file _path TEXT NOT NULL ,
original _filename TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_hr_template_documents_created ON hr_template_documents(created_at DESC) ` ) ;
console . log ( '[dbInit] Таблица hr_template_documents создана' ) ;
}
// Платежный календарь: справочник статей доходов/расходов
const paymentCategoriesCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'payment_categories' `
) ;
if ( paymentCategoriesCheck . rows . length === 0 ) {
const paymentCategoriesPath = path . join ( _ _dirname , 'migrations' , 'create_payment_categories.sql' ) ;
if ( fs . existsSync ( paymentCategoriesPath ) ) {
const sql = fs . readFileSync ( paymentCategoriesPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_payment_categories.sql применена успешно' ) ;
}
}
// Платежный календарь: записи (расходы/поступления)
const paymentCalendarEntriesCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'payment_calendar_entries' `
) ;
if ( paymentCalendarEntriesCheck . rows . length === 0 ) {
const paymentCalendarPath = path . join ( _ _dirname , 'migrations' , 'create_payment_calendar_entries.sql' ) ;
if ( fs . existsSync ( paymentCalendarPath ) ) {
const sql = fs . readFileSync ( paymentCalendarPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_payment_calendar_entries.sql применена успешно' ) ;
}
}
// Строки отчёта по задолженности (лицевые счета, долги)
const debtorReportDataCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'debtor_report_data' `
) ;
if ( debtorReportDataCheck . rows . length === 0 ) {
const debtorReportDataPath = path . join ( _ _dirname , 'migrations' , 'create_debtor_report_data.sql' ) ;
if ( fs . existsSync ( debtorReportDataPath ) ) {
const sql = fs . readFileSync ( debtorReportDataPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_debtor_report_data.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_debtor_report_data.sql не найден' ) ;
}
}
// Банки и кошельки (наличка)
const financeAccountsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'finance_accounts' `
) ;
if ( financeAccountsCheck . rows . length === 0 ) {
const accountsPath = path . join ( _ _dirname , 'migrations' , 'create_finance_accounts.sql' ) ;
if ( fs . existsSync ( accountsPath ) ) {
const sql = fs . readFileSync ( accountsPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_finance_accounts.sql применена успешно' ) ;
}
}
// Лицевые счета домов (отдельная таблица — защита от перезаписи при PUT /buildings/:id)
const buildingPersonalAccountsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'building_personal_accounts' `
) ;
if ( buildingPersonalAccountsCheck . rows . length === 0 ) {
const bpaPath = path . join ( _ _dirname , 'migrations' , 'create_building_personal_accounts.sql' ) ;
if ( fs . existsSync ( bpaPath ) ) {
const sql = fs . readFileSync ( bpaPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_building_personal_accounts.sql применена успешно' ) ;
}
}
// Справочник должностей (панель управления)
const positionsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'positions' `
) ;
if ( positionsCheck . rows . length === 0 ) {
const positionsPath = path . join ( _ _dirname , 'migrations' , 'create_positions.sql' ) ;
if ( fs . existsSync ( positionsPath ) ) {
const sql = fs . readFileSync ( positionsPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_positions.sql применена успешно' ) ;
}
}
// Таблица назначений сотрудников на участки (многие ко многим)
const employeeDistrictsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'employee_districts' `
) ;
if ( employeeDistrictsCheck . rows . length === 0 ) {
const employeeDistrictsPath = path . join ( _ _dirname , 'migrations' , 'create_employee_districts.sql' ) ;
if ( fs . existsSync ( employeeDistrictsPath ) ) {
const sql = fs . readFileSync ( employeeDistrictsPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция create_employee_districts.sql применена успешно' ) ;
}
}
// Применяем миграцию ролей пользователей
const userRolesCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'user_roles' `
) ;
if ( userRolesCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаем таблицу user_roles...' ) ;
const userRolesPath = path . join ( _ _dirname , 'migrations' , 'create_user_roles.sql' ) ;
if ( fs . existsSync ( userRolesPath ) ) {
const userRolesSQL = fs . readFileSync ( userRolesPath , 'utf8' ) ;
await client . query ( userRolesSQL ) ;
console . log ( '[dbInit] Миграция create_user_roles.sql применена успешно' ) ;
} else {
console . warn ( '[dbInit] Файл миграции create_user_roles.sql не найден' ) ;
}
}
// Применяем миграцию добавления item_type в payment_invoices (если таблица уже существует)
const paymentInvoicesItemTypeCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'payment_invoices' `
) ;
if ( paymentInvoicesItemTypeCheck . rows . length > 0 ) {
const itemTypeMigrationPath = path . join ( _ _dirname , 'migrations' , 'add_item_type_to_payment_invoices.sql' ) ;
if ( fs . existsSync ( itemTypeMigrationPath ) ) {
try {
const itemTypeMigrationSQL = fs . readFileSync ( itemTypeMigrationPath , 'utf8' ) ;
await client . query ( itemTypeMigrationSQL ) ;
console . log ( '[dbInit] Миграция add_item_type_to_payment_invoices.sql применена успешно' ) ;
} catch ( err ) {
// Игнорируем ошибки, если колонка уже существует
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_item_type_to_payment_invoices.sql:' , err . message ) ;
}
}
}
}
// Применяем миграцию добавления файлов закрывающих документов в payment_invoices
if ( paymentInvoicesItemTypeCheck . rows . length > 0 ) {
const closingDocsMigrationPath = path . join ( _ _dirname , 'migrations' , 'add_closing_docs_files_to_payment_invoices.sql' ) ;
if ( fs . existsSync ( closingDocsMigrationPath ) ) {
try {
const closingDocsSQL = fs . readFileSync ( closingDocsMigrationPath , 'utf8' ) ;
await client . query ( closingDocsSQL ) ;
console . log ( '[dbInit] Миграция add_closing_docs_files_to_payment_invoices.sql применена успешно' ) ;
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_closing_docs_files_to_payment_invoices.sql:' , err . message ) ;
}
}
}
}
// Применяем миграцию добавления purpose_type 'event' и purpose_event_id в payment_invoices
const prEventsExistsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pr_events' `
) ;
if ( paymentInvoicesItemTypeCheck . rows . length > 0 && prEventsExistsCheck . rows . length > 0 ) {
const addEventToInvoicesPath = path . join ( _ _dirname , 'migrations' , 'add_event_to_payment_invoices.sql' ) ;
if ( fs . existsSync ( addEventToInvoicesPath ) ) {
try {
const addEventSQL = fs . readFileSync ( addEventToInvoicesPath , 'utf8' ) ;
await client . query ( addEventSQL ) ;
console . log ( '[dbInit] Миграция add_event_to_payment_invoices.sql применена успешно' ) ;
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_event_to_payment_invoices.sql:' , err . message ) ;
}
}
}
}
// Применяем миграцию plan_item_id / plan_item_building_id в payment_invoices (план работ ↔ счета)
if ( paymentInvoicesItemTypeCheck . rows . length > 0 ) {
const planItemMigrationPath = path . join ( _ _dirname , 'migrations' , 'add_plan_item_to_payment_invoices.sql' ) ;
if ( fs . existsSync ( planItemMigrationPath ) ) {
try {
const planItemMigrationSQL = fs . readFileSync ( planItemMigrationPath , 'utf8' ) ;
await client . query ( planItemMigrationSQL ) ;
console . log ( '[dbInit] Миграция add_plan_item_to_payment_invoices.sql применена успешно' ) ;
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_plan_item_to_payment_invoices.sql:' , err . message ) ;
}
}
}
}
// payment_ref, is_cash, postponed_date, cancel_reason в payment_invoices
if ( paymentInvoicesItemTypeCheck . rows . length > 0 ) {
const paymentRefPath = path . join ( _ _dirname , 'migrations' , 'add_payment_ref_and_is_cash_to_payment_invoices.sql' ) ;
if ( fs . existsSync ( paymentRefPath ) ) {
try {
const sql = fs . readFileSync ( paymentRefPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция add_payment_ref_and_is_cash_to_payment_invoices.sql применена успешно' ) ;
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка add_payment_ref_and_is_cash_to_payment_invoices.sql:' , err . message ) ;
}
}
}
}
// Увеличиваем длину поля status в payment_invoices, если она слишком маленькая
// (для значения 'pending_finance_manager_approval' нужно больше 30 символов)
try {
const statusLengthCheck = await client . query ( `
SELECT character _maximum _length
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = 'payment_invoices'
AND column _name = 'status'
` );
if ( statusLengthCheck . rows . length > 0 ) {
const maxLen = statusLengthCheck . rows [ 0 ] . character _maximum _length ;
if ( maxLen !== null && maxLen < 40 ) {
console . log ( '[dbInit] Увеличиваем длину payment_invoices.status до VARCHAR(50)...' ) ;
await client . query ( `
ALTER TABLE payment _invoices
ALTER COLUMN status TYPE VARCHAR ( 50 )
` );
console . log ( '[dbInit] Колонка payment_invoices.status успешно изменена на VARCHAR(50)' ) ;
}
}
} catch ( err ) {
console . warn ( '[dbInit] Н е удалось обновить длину payment_invoices.status:' , err . message ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграций колонок:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для таблицы candidate _events
* /
async function applyCandidateEventsMigration ( client ) {
try {
// Проверяем существование таблицы candidate_events
const tableCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'candidate_events' `
) ;
if ( tableCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблица candidate_events не найдена, создаем...' ) ;
// Создаем типы ENUM если их нет
await client . query ( `
DO $$
BEGIN
IF NOT EXISTS ( SELECT 1 FROM pg _type WHERE typname = 'candidate_event_type' ) THEN
CREATE TYPE candidate _event _type AS ENUM (
'call' , 'interview_1' , 'interview_2' , 'interview_3' , 'test_task' ,
'offer' , 'offer_accepted' , 'offer_rejected' , 'probation_start' ,
'hired' , 'rejected' , 'other'
) ;
END IF ;
END$$ ;
` );
await client . query ( `
DO $$
BEGIN
IF NOT EXISTS ( SELECT 1 FROM pg _type WHERE typname = 'candidate_event_result' ) THEN
CREATE TYPE candidate _event _result AS ENUM ( 'success' , 'failed' , 'pending' , 'cancelled' ) ;
END IF ;
END$$ ;
` );
// Создаем таблицу
await client . query ( `
CREATE TABLE candidate _events (
id VARCHAR ( 50 ) PRIMARY KEY ,
candidate _id VARCHAR ( 50 ) NOT NULL REFERENCES candidates ( id ) ON DELETE CASCADE ,
event _type candidate _event _type NOT NULL ,
event _date TIMESTAMPTZ NOT NULL ,
notes TEXT ,
result candidate _event _result DEFAULT 'pending' ,
interviewer TEXT ,
location TEXT ,
duration _minutes INTEGER ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
// Создаем индексы
await client . query ( `
CREATE INDEX IF NOT EXISTS idx _candidate _events _candidate ON candidate _events ( candidate _id )
` );
await client . query ( `
CREATE INDEX IF NOT EXISTS idx _candidate _events _type ON candidate _events ( event _type )
` );
await client . query ( `
CREATE INDEX IF NOT EXISTS idx _candidate _events _date ON candidate _events ( event _date DESC )
` );
console . log ( '[dbInit] Таблица candidate_events успешно создана' ) ;
} else {
console . log ( '[dbInit] Таблица candidate_events уже существует' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции candidate_events:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для модуля обучения ( инструктажи и курсы )
* /
async function applyTrainingModuleMigration ( client ) {
try {
const tableCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'training_programs' `
) ;
if ( tableCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблицы модуля обучения не найдены, создаем...' ) ;
// Читаем SQL миграцию
const migrationPath = path . join ( _ _dirname , 'migrate_training_module.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Таблицы модуля обучения успешно созданы' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_training_module.sql не найден, создаем таблицы вручную...' ) ;
// Создаем типы
await client . query ( `
DO $$
BEGIN
IF NOT EXISTS ( SELECT 1 FROM pg _type WHERE typname = 'training_type' ) THEN
CREATE TYPE training _type AS ENUM (
'instruction' , 'course' , 'certification' , 'exam' , 'other'
) ;
END IF ;
END$$ ;
` );
await client . query ( `
DO $$
BEGIN
IF NOT EXISTS ( SELECT 1 FROM pg _type WHERE typname = 'training_category' ) THEN
CREATE TYPE training _category AS ENUM (
'safety' , 'fire_safety' , 'electrical' , 'first_aid' ,
'professional' , 'compliance' , 'other'
) ;
END IF ;
END$$ ;
` );
await client . query ( `
DO $$
BEGIN
IF NOT EXISTS ( SELECT 1 FROM pg _type WHERE typname = 'training_status' ) THEN
CREATE TYPE training _status AS ENUM (
'not_started' , 'in_progress' , 'completed' , 'failed' , 'expired' , 'cancelled'
) ;
END IF ;
END$$ ;
` );
// Создаем таблицы
await client . query ( `
CREATE TABLE IF NOT EXISTS training _programs (
id VARCHAR ( 50 ) PRIMARY KEY ,
title TEXT NOT NULL ,
description TEXT ,
type training _type NOT NULL ,
category training _category NOT NULL ,
duration _hours NUMERIC ( 5 , 2 ) ,
validity _months INTEGER ,
is _required BOOLEAN DEFAULT false ,
required _for _positions TEXT [ ] ,
instructor _name TEXT ,
materials _url TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( `
CREATE TABLE IF NOT EXISTS employee _training (
id BIGSERIAL PRIMARY KEY ,
employee _id VARCHAR ( 50 ) NOT NULL REFERENCES employees ( id ) ON DELETE CASCADE ,
program _id VARCHAR ( 50 ) NOT NULL REFERENCES training _programs ( id ) ON DELETE CASCADE ,
status training _status NOT NULL DEFAULT 'not_started' ,
start _date DATE ,
completion _date DATE ,
expiry _date DATE ,
score NUMERIC ( 5 , 2 ) ,
passed BOOLEAN ,
certificate _number TEXT ,
certificate _url TEXT ,
notes TEXT ,
instructor _name TEXT ,
location TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( employee _id , program _id )
)
` );
await client . query ( `
CREATE TABLE IF NOT EXISTS employee _training _history (
id BIGSERIAL PRIMARY KEY ,
employee _training _id BIGINT NOT NULL REFERENCES employee _training ( id ) ON DELETE CASCADE ,
status training _status NOT NULL ,
completion _date DATE ,
score NUMERIC ( 5 , 2 ) ,
passed BOOLEAN ,
certificate _number TEXT ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
// Создаем индексы
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_training_programs_type ON training_programs(type) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_training_programs_category ON training_programs(category) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_employee_training_employee ON employee_training(employee_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_employee_training_program ON employee_training(program_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_employee_training_status ON employee_training(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_employee_training_expiry ON employee_training(expiry_date) ` ) ;
console . log ( '[dbInit] Таблицы модуля обучения успешно созданы' ) ;
}
} else {
console . log ( '[dbInit] Таблицы модуля обучения уже существуют' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции модуля обучения:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для таблиц офиса ( заявки на ТМЦ , склад , документооборот )
* /
async function applyOfficeTablesMigration ( client ) {
try {
// Проверяем и создаем таблицу office_supply_requests
const supplyRequestsCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'office_supply_requests' `
) ;
if ( supplyRequestsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблица office_supply_requests не найдена, создаем...' ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS office _supply _requests (
id BIGSERIAL PRIMARY KEY ,
requester _name TEXT NOT NULL ,
category VARCHAR ( 50 ) NOT NULL ,
item _name TEXT NOT NULL ,
quantity INTEGER DEFAULT 1 ,
issued _quantity INTEGER DEFAULT 0 ,
unit VARCHAR ( 20 ) DEFAULT 'шт.' ,
amount NUMERIC ( 10 , 2 ) DEFAULT 0 ,
priority VARCHAR ( 10 ) NOT NULL DEFAULT 'medium' CHECK ( priority IN ( 'low' , 'medium' , 'high' , 'urgent' ) ) ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'new' CHECK ( status IN ( 'new' , 'approved' , 'ordered' , 'received' , 'canceled' , 'archived' , 'collected' ) ) ,
approved _by TEXT ,
approved _at TIMESTAMPTZ ,
ordered _at TIMESTAMPTZ ,
received _at TIMESTAMPTZ ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_supply_requests_status ON office_supply_requests(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_supply_requests_requester ON office_supply_requests(requester_name) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_supply_requests_created ON office_supply_requests(created_at DESC) ` ) ;
console . log ( '[dbInit] Таблица office_supply_requests успешно создана' ) ;
} else {
// Проверяем наличие поля issued_quantity
const columnCheck = await client . query ( `
SELECT column _name
FROM information _schema . columns
WHERE table _name = 'office_supply_requests'
AND column _name = 'issued_quantity'
` );
if ( columnCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем поле issued_quantity в office_supply_requests...' ) ;
await client . query ( `
ALTER TABLE office _supply _requests
ADD COLUMN IF NOT EXISTS issued _quantity INTEGER DEFAULT 0
` );
console . log ( '[dbInit] Поле issued_quantity успешно добавлено' ) ;
}
}
// Проверяем и создаем таблицу office_inventory
const inventoryCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'office_inventory' `
) ;
if ( inventoryCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблица office_inventory не найдена, создаем...' ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS office _inventory (
id BIGSERIAL PRIMARY KEY ,
name TEXT NOT NULL ,
category VARCHAR ( 50 ) ,
quantity NUMERIC ( 10 , 2 ) NOT NULL DEFAULT 0 ,
unit VARCHAR ( 20 ) NOT NULL DEFAULT 'шт.' ,
min _threshold NUMERIC ( 10 , 2 ) NOT NULL DEFAULT 0 ,
last _restock DATE ,
last _restock _by TEXT ,
location TEXT ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_office_inventory_category ON office_inventory(category) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_office_inventory_name ON office_inventory(name) ` ) ;
console . log ( '[dbInit] Таблица office_inventory успешно создана' ) ;
}
// Проверяем и создаем таблицу office_documents
const documentsCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'office_documents' `
) ;
if ( documentsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблица office_documents не найдена, создаем...' ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS office _documents (
id BIGSERIAL PRIMARY KEY ,
reg _number TEXT NOT NULL UNIQUE ,
title TEXT NOT NULL ,
correspondent TEXT NOT NULL ,
document _type VARCHAR ( 20 ) NOT NULL CHECK ( document _type IN ( 'incoming' , 'outgoing' ) ) ,
letter _type VARCHAR ( 10 ) DEFAULT 'paper' CHECK ( letter _type IN ( 'email' , 'paper' ) ) ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'registered' CHECK ( status IN ( 'registered' , 'processed' , 'sent' , 'archived' ) ) ,
date DATE NOT NULL ,
assigned _to TEXT ,
tracking _number TEXT ,
file _url TEXT ,
notes TEXT ,
created _by TEXT NOT NULL ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_office_documents_reg_number ON office_documents(reg_number) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_office_documents_status ON office_documents(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_office_documents_type ON office_documents(document_type) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_office_documents_date ON office_documents(date DESC) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_office_documents_created_by ON office_documents(created_by) ` ) ;
console . log ( '[dbInit] Таблица office_documents успешно создана' ) ;
} else {
// Таблица существует, проверяем наличие поля letter_type
try {
const columnCheck = await client . query ( `
SELECT column _name
FROM information _schema . columns
WHERE table _name = 'office_documents'
AND column _name = 'letter_type'
` );
if ( columnCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем поле letter_type в office_documents...' ) ;
await client . query ( `
ALTER TABLE office _documents
ADD COLUMN letter _type VARCHAR ( 10 ) DEFAULT 'paper'
CHECK ( letter _type IN ( 'email' , 'paper' ) )
` );
console . log ( '[dbInit] Поле letter_type успешно добавлено' ) ;
}
} catch ( err ) {
console . warn ( '[dbInit] Предупреждение при добавлении letter_type:' , err . message ) ;
}
}
// Проверяем наличие поля next_maintenance_date в office_equipment
const equipmentColumnCheck = await client . query (
` SELECT column_name
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = 'office_equipment'
AND column _name = 'next_maintenance_date' `
) ;
if ( equipmentColumnCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем отсутствующую колонку next_maintenance_date в таблицу office_equipment...' ) ;
await client . query ( `
ALTER TABLE office _equipment
ADD COLUMN IF NOT EXISTS next _maintenance _date DATE
` );
console . log ( '[dbInit] Колонка next_maintenance_date успешно добавлена' ) ;
}
// Проверяем наличие поля conclusions в meetings
const meetingsColumnCheck = await client . query (
` SELECT column_name
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = 'meetings'
AND column _name = 'conclusions' `
) ;
if ( meetingsColumnCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем отсутствующую колонку conclusions в таблицу meetings...' ) ;
await client . query ( `
ALTER TABLE meetings
ADD COLUMN IF NOT EXISTS conclusions TEXT
` );
console . log ( '[dbInit] Колонка conclusions успешно добавлена' ) ;
}
// Проверяем и создаем таблицы для заказов
const ordersCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'office_orders' `
) ;
if ( ordersCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблицы заказов не найдены, создаем...' ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS office _orders (
id BIGSERIAL PRIMARY KEY ,
order _number TEXT UNIQUE NOT NULL ,
title TEXT NOT NULL ,
description TEXT ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'draft' CHECK ( status IN ( 'draft' , 'waiting_quote' , 'quotes_received' , 'approved' , 'ordered' , 'received' , 'canceled' ) ) ,
total _amount NUMERIC ( 10 , 2 ) DEFAULT 0 ,
supplier _name TEXT ,
supplier _contact TEXT ,
invoice _id BIGINT ,
invoice _url TEXT ,
expected _date DATE ,
received _date DATE ,
notes TEXT ,
created _by TEXT NOT NULL ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( `
CREATE TABLE IF NOT EXISTS office _order _items (
id BIGSERIAL PRIMARY KEY ,
order _id BIGINT NOT NULL REFERENCES office _orders ( id ) ON DELETE CASCADE ,
request _id BIGINT NOT NULL REFERENCES office _supply _requests ( id ) ON DELETE CASCADE ,
quantity INTEGER DEFAULT 1 ,
unit _price NUMERIC ( 10 , 2 ) DEFAULT 0 ,
total _price NUMERIC ( 10 , 2 ) DEFAULT 0 ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( order _id , request _id )
)
` );
await client . query ( `
CREATE TABLE IF NOT EXISTS office _order _quotes (
id BIGSERIAL PRIMARY KEY ,
order _id BIGINT NOT NULL REFERENCES office _orders ( id ) ON DELETE CASCADE ,
supplier _name TEXT NOT NULL ,
supplier _contact TEXT ,
total _amount NUMERIC ( 10 , 2 ) NOT NULL ,
quote _file _url TEXT ,
notes TEXT ,
is _selected BOOLEAN DEFAULT FALSE ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_orders_status ON office_orders(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_orders_created ON office_orders(created_at DESC) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_order_items_order ON office_order_items(order_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_order_items_request ON office_order_items(request_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_order_quotes_order ON office_order_quotes(order_id) ` ) ;
console . log ( '[dbInit] Таблицы заказов успешно созданы' ) ;
}
// Проверяем и добавляем поля для платного ремонта в office_repair_requests
const repairRequestsCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'office_repair_requests' `
) ;
if ( repairRequestsCheck . rows . length > 0 ) {
// Таблица существует, проверяем наличие полей для платного ремонта
const costColumnCheck = await client . query ( `
SELECT column _name
FROM information _schema . columns
WHERE table _name = 'office_repair_requests'
AND column _name = 'cost'
` );
if ( costColumnCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем поля для платного ремонта в office_repair_requests...' ) ;
await client . query ( `
ALTER TABLE office _repair _requests
ADD COLUMN IF NOT EXISTS is _paid BOOLEAN DEFAULT FALSE ,
ADD COLUMN IF NOT EXISTS cost NUMERIC ( 10 , 2 ) DEFAULT 0 ,
ADD COLUMN IF NOT EXISTS cost _estimated BOOLEAN DEFAULT FALSE ,
ADD COLUMN IF NOT EXISTS invoice _id BIGINT ,
ADD COLUMN IF NOT EXISTS invoice _url TEXT
` );
console . log ( '[dbInit] Поля для платного ремонта успешно добавлены' ) ;
}
// Поля для статусов: ожидание поставки, увезли на ремонт, договорились с подрядчиком
const statusFieldsCheck = await client . query ( `
SELECT column _name FROM information _schema . columns
WHERE table _name = 'office_repair_requests' AND column _name = 'taken_for_repair_deadline'
` );
if ( statusFieldsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем поля статусов ремонта в office_repair_requests...' ) ;
await client . query ( ` ALTER TABLE office_repair_requests ADD COLUMN IF NOT EXISTS waiting_delivery_deadline TEXT ` ) ;
await client . query ( ` ALTER TABLE office_repair_requests ADD COLUMN IF NOT EXISTS waiting_delivery_contacts TEXT ` ) ;
await client . query ( ` ALTER TABLE office_repair_requests ADD COLUMN IF NOT EXISTS taken_for_repair_deadline TEXT ` ) ;
await client . query ( ` ALTER TABLE office_repair_requests ADD COLUMN IF NOT EXISTS taken_for_repair_contacts TEXT ` ) ;
await client . query ( ` ALTER TABLE office_repair_requests ADD COLUMN IF NOT EXISTS agreed_contractor_price NUMERIC(10, 2) ` ) ;
console . log ( '[dbInit] Поля статусов ремонта успешно добавлены' ) ;
}
// Добавляем недостающие значения в enum repair_request_status (если БД создана по старой схеме)
const repairStatusEnumValues = [ 'agreed_with_contractor' , 'search_contractor' , 'waiting_delivery' , 'taken_for_repair' , 'self_repair' ] ;
for ( const enumVal of repairStatusEnumValues ) {
try {
await client . query ( ` ALTER TYPE repair_request_status ADD VALUE IF NOT EXISTS ' ${ enumVal } ' ` ) ;
} catch ( err ) {
if ( ! err . message || ! err . message . includes ( 'already exists' ) ) {
console . warn ( ` [dbInit] Предупреждение при добавлении значения ${ enumVal } в repair_request_status: ` , err . message ) ;
}
}
}
// Таблица истории оборудования: создаём, если не существует
const historyTableCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'office_equipment_history' `
) ;
if ( historyTableCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаём таблицу office_equipment_history...' ) ;
const typeCheck = await client . query (
` SELECT 1 FROM pg_type WHERE typname = 'office_equipment_history_type' `
) ;
if ( typeCheck . rows . length === 0 ) {
await client . query ( `
CREATE TYPE office _equipment _history _type AS ENUM ( 'purchase' , 'issue' , 'transfer' , 'repair' , 'write_off' )
` );
}
await client . query ( `
CREATE TABLE IF NOT EXISTS office _equipment _history (
id BIGSERIAL PRIMARY KEY ,
equipment _id BIGINT NOT NULL REFERENCES office _equipment ( id ) ON DELETE CASCADE ,
event _type office _equipment _history _type NOT NULL ,
event _date DATE NOT NULL ,
assigned _to TEXT ,
assigned _from TEXT ,
reason TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_equipment_history_equipment ON office_equipment_history(equipment_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_equipment_history_date ON office_equipment_history(event_date DESC) ` ) ;
console . log ( '[dbInit] Таблица office_equipment_history создана' ) ;
} else {
const historyCols = await client . query (
` SELECT column_name FROM information_schema.columns WHERE table_name = 'office_equipment_history' AND column_name IN ('assigned_from', 'assigned_to') `
) ;
const hasFrom = historyCols . rows . some ( r => r . column _name === 'assigned_from' ) ;
const hasTo = historyCols . rows . some ( r => r . column _name === 'assigned_to' ) ;
if ( ! hasFrom ) {
await client . query ( ` ALTER TABLE office_equipment_history ADD COLUMN IF NOT EXISTS assigned_from TEXT ` ) ;
console . log ( '[dbInit] Добавлена колонка assigned_from в office_equipment_history' ) ;
}
if ( ! hasTo ) {
await client . query ( ` ALTER TABLE office_equipment_history ADD COLUMN IF NOT EXISTS assigned_to TEXT ` ) ;
console . log ( '[dbInit] Добавлена колонка assigned_to в office_equipment_history' ) ;
}
}
}
// Создать office_equipment_history при наличии office_equipment (если ещё не создали выше)
const officeEquipmentCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'office_equipment' `
) ;
if ( officeEquipmentCheck . rows . length > 0 ) {
const historyExists = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'office_equipment_history' `
) ;
if ( historyExists . rows . length === 0 ) {
console . log ( '[dbInit] Создаём таблицу office_equipment_history (при наличии office_equipment)...' ) ;
try {
const typeCheck = await client . query ( ` SELECT 1 FROM pg_type WHERE typname = 'office_equipment_history_type' ` ) ;
if ( typeCheck . rows . length === 0 ) {
await client . query ( ` CREATE TYPE office_equipment_history_type AS ENUM ('purchase', 'issue', 'transfer', 'repair', 'write_off') ` ) ;
}
await client . query ( `
CREATE TABLE IF NOT EXISTS office _equipment _history (
id BIGSERIAL PRIMARY KEY ,
equipment _id BIGINT NOT NULL REFERENCES office _equipment ( id ) ON DELETE CASCADE ,
event _type office _equipment _history _type NOT NULL ,
event _date DATE NOT NULL ,
assigned _to TEXT ,
assigned _from TEXT ,
reason TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_equipment_history_equipment ON office_equipment_history(equipment_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_equipment_history_date ON office_equipment_history(event_date DESC) ` ) ;
console . log ( '[dbInit] Таблица office_equipment_history создана' ) ;
} catch ( histErr ) {
console . warn ( '[dbInit] Ошибка создания office_equipment_history:' , histErr . message ) ;
}
}
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции таблиц офиса:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции PR и NPS модуля
* /
async function applyPRNPSMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrate_pr_nps.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию PR/NPS из migrate_pr_nps.sql...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция PR/NPS успешно применена' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_pr_nps.sql не найден, создаем таблицы вручную...' ) ;
// Создаем таблицы PR/NPS вручную
await client . query ( `
CREATE TABLE IF NOT EXISTS reviews (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE CASCADE ,
source VARCHAR ( 20 ) NOT NULL CHECK ( source IN ( 'yandex_maps' , '2gis' , 'internal' , 'other' ) ) ,
source _url TEXT ,
author _name TEXT ,
text TEXT NOT NULL ,
rating INTEGER NOT NULL CHECK ( rating >= 1 AND rating <= 10 ) ,
date DATE NOT NULL ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'new' CHECK ( status IN ( 'new' , 'processed' , 'archived' ) ) ,
processed _at TIMESTAMPTZ ,
processed _by TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_reviews_building ON reviews(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_reviews_source ON reviews(source) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_reviews_status ON reviews(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_reviews_date ON reviews(date DESC) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS incidents (
id BIGSERIAL PRIMARY KEY ,
review _id BIGINT REFERENCES reviews ( id ) ON DELETE SET NULL ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE CASCADE ,
type VARCHAR ( 50 ) NOT NULL CHECK ( type IN ( 'property_damage' , 'debtor_complaint' , 'service_quality' , 'other' ) ) ,
title TEXT NOT NULL ,
description TEXT NOT NULL ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'new' CHECK ( status IN ( 'new' , 'in_progress' , 'resolved' , 'closed' ) ) ,
priority VARCHAR ( 10 ) NOT NULL DEFAULT 'medium' CHECK ( priority IN ( 'low' , 'medium' , 'high' , 'urgent' ) ) ,
assigned _to TEXT ,
created _by TEXT NOT NULL ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
resolved _at TIMESTAMPTZ ,
resolution _notes TEXT ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_incidents_building ON incidents(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_incidents_type ON incidents(type) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_incidents_assigned ON incidents(assigned_to) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_incidents_review ON incidents(review_id) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS resident _reports (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) NOT NULL REFERENCES buildings ( id ) ON DELETE CASCADE ,
month VARCHAR ( 20 ) NOT NULL ,
period _start DATE NOT NULL ,
period _end DATE NOT NULL ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'draft' CHECK ( status IN ( 'draft' , 'published' ) ) ,
published _at TIMESTAMPTZ ,
content JSONB ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_resident_reports_building ON resident_reports(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_resident_reports_status ON resident_reports(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_resident_reports_period ON resident_reports(period_start) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS work _photos (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) NOT NULL REFERENCES buildings ( id ) ON DELETE CASCADE ,
resident _report _id BIGINT REFERENCES resident _reports ( id ) ON DELETE SET NULL ,
task _id VARCHAR ( 50 ) ,
work _name TEXT NOT NULL ,
work _date DATE NOT NULL ,
description TEXT ,
photo _before _url TEXT ,
photo _after _url TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_work_photos_building ON work_photos(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_work_photos_report ON work_photos(resident_report_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_work_photos_date ON work_photos(work_date DESC) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_work_photos_task ON work_photos(task_id) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS parsing _settings (
id BIGSERIAL PRIMARY KEY ,
source VARCHAR ( 20 ) NOT NULL UNIQUE CHECK ( source IN ( 'yandex_maps' , '2gis' ) ) ,
enabled BOOLEAN NOT NULL DEFAULT TRUE ,
url _template TEXT ,
api _key TEXT ,
parsing _interval _hours INTEGER DEFAULT 24 ,
last _parsed _at TIMESTAMPTZ ,
settings JSONB ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_parsing_settings_source ON parsing_settings(source) ` ) ;
// Инициализируем настройки парсинга, если их нет
const existingSettings = await client . query ( 'SELECT COUNT(*) as count FROM parsing_settings' ) ;
if ( parseInt ( existingSettings . rows [ 0 ] . count ) === 0 ) {
await client . query ( `
INSERT INTO parsing _settings ( source , enabled , url _template , parsing _interval _hours )
VALUES
( 'yandex_maps' , FALSE , '' , 24 ) ,
( '2gis' , FALSE , '' , 24 )
` );
console . log ( '[dbInit] Настройки парсинга инициализированы' ) ;
}
console . log ( '[dbInit] Таблицы PR/NPS успешно созданы' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции PR/NPS:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для таблиц NPS опросов
* /
async function applyNPSSurveysMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrate_nps_surveys.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию NPS опросов из migrate_nps_surveys.sql...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция NPS опросов успешно применена' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_nps_surveys.sql не найден, создаем таблицы вручную...' ) ;
// Создаем таблицы NPS опросов вручную
await client . query ( `
CREATE TABLE IF NOT EXISTS nps _surveys (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) NOT NULL REFERENCES buildings ( id ) ON DELETE CASCADE ,
title TEXT NOT NULL DEFAULT 'Опрос удовлетворенности' ,
description TEXT ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'draft' CHECK ( status IN ( 'draft' , 'active' , 'closed' ) ) ,
access _key TEXT NOT NULL UNIQUE ,
published _at TIMESTAMPTZ ,
expires _at TIMESTAMPTZ ,
created _by TEXT NOT NULL ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_surveys_building ON nps_surveys(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_surveys_status ON nps_surveys(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_surveys_access_key ON nps_surveys(access_key) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS nps _responses (
id BIGSERIAL PRIMARY KEY ,
survey _id BIGINT NOT NULL REFERENCES nps _surveys ( id ) ON DELETE CASCADE ,
building _id VARCHAR ( 50 ) NOT NULL REFERENCES buildings ( id ) ON DELETE CASCADE ,
score INTEGER NOT NULL CHECK ( score >= 0 AND score <= 10 ) ,
comment TEXT ,
respondent _name TEXT ,
apartment TEXT ,
phone TEXT ,
email TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_responses_survey ON nps_responses(survey_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_responses_building ON nps_responses(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_responses_score ON nps_responses(score) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_responses_created ON nps_responses(created_at DESC) ` ) ;
console . log ( '[dbInit] Таблицы NPS опросов успешно созданы' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции NPS опросов:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для таблицы агрегированных показателей NPS по дому
* /
async function applyNPSBuildingStatsMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrate_nps_building_stats.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию nps_building_stats...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Таблица nps_building_stats успешно создана' ) ;
} else {
await client . query ( `
CREATE TABLE IF NOT EXISTS nps _building _stats (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) NOT NULL REFERENCES buildings ( id ) ON DELETE CASCADE ,
period _start DATE NOT NULL ,
period _end DATE NOT NULL ,
total _responses INTEGER NOT NULL DEFAULT 0 ,
nps _score INTEGER NOT NULL DEFAULT 0 ,
avg _score NUMERIC ( 4 , 2 ) NOT NULL DEFAULT 0 ,
promoters INTEGER NOT NULL DEFAULT 0 ,
passives INTEGER NOT NULL DEFAULT 0 ,
detractors INTEGER NOT NULL DEFAULT 0 ,
promoter _percent NUMERIC ( 5 , 2 ) DEFAULT 0 ,
detractor _percent NUMERIC ( 5 , 2 ) DEFAULT 0 ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( building _id , period _start , period _end )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_building_stats_building ON nps_building_stats(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_nps_building_stats_period ON nps_building_stats(period_start, period_end) ` ) ;
console . log ( '[dbInit] Таблица nps_building_stats успешно создана' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции nps_building_stats:' , error . message ) ;
}
}
/ * *
* Применение миграции для таблицы данных отчетов для жителей ( дом + период )
* /
async function applyResidentReportDataMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrate_resident_report_data.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию resident_report_data...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Таблица resident_report_data успешно создана' ) ;
} else {
await client . query ( `
CREATE TABLE IF NOT EXISTS resident _report _data (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) NOT NULL REFERENCES buildings ( id ) ON DELETE CASCADE ,
period _start DATE NOT NULL ,
period _end DATE NOT NULL ,
period _month INTEGER NOT NULL ,
period _year INTEGER NOT NULL ,
nps _score INTEGER NOT NULL DEFAULT 0 ,
nps _total _responses INTEGER NOT NULL DEFAULT 0 ,
nps _avg _score NUMERIC ( 4 , 2 ) DEFAULT 0 ,
nps _promoters INTEGER NOT NULL DEFAULT 0 ,
nps _passives INTEGER NOT NULL DEFAULT 0 ,
nps _detractors INTEGER NOT NULL DEFAULT 0 ,
apps _total INTEGER NOT NULL DEFAULT 0 ,
apps _completed INTEGER NOT NULL DEFAULT 0 ,
apps _quality INTEGER NOT NULL DEFAULT 0 ,
tasks _total INTEGER NOT NULL DEFAULT 0 ,
tasks _completed INTEGER NOT NULL DEFAULT 0 ,
funds _collected NUMERIC ( 14 , 2 ) NOT NULL DEFAULT 0 ,
funds _spent NUMERIC ( 14 , 2 ) NOT NULL DEFAULT 0 ,
funds _balance NUMERIC ( 14 , 2 ) NOT NULL DEFAULT 0 ,
debt _cases _won INTEGER NOT NULL DEFAULT 0 ,
debt _collected NUMERIC ( 14 , 2 ) NOT NULL DEFAULT 0 ,
expenses _total NUMERIC ( 14 , 2 ) NOT NULL DEFAULT 0 ,
expenses _by _category JSONB ,
events JSONB ,
work _photos JSONB ,
plan _items JSONB ,
snapshot JSONB ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( building _id , period _start , period _end )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_resident_report_data_building ON resident_report_data(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_resident_report_data_period ON resident_report_data(period_year, period_month) ` ) ;
console . log ( '[dbInit] Таблица resident_report_data успешно создана' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции resident_report_data:' , error . message ) ;
}
}
/ * *
* Применение миграции для настроек компании
* /
async function applyCompanySettingsMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrate_company_settings.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию настроек компании из migrate_company_settings.sql...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция настроек компании успешно применена' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_company_settings.sql не найден, создаем таблицу вручную...' ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS company _settings (
id BIGSERIAL PRIMARY KEY ,
name TEXT NOT NULL DEFAULT 'Управляющая компания' ,
full _name TEXT ,
address TEXT ,
phone TEXT ,
email TEXT ,
website TEXT ,
license _number TEXT ,
license _valid _until DATE ,
logo _url TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
// Вставляем дефолтные данные
await client . query ( `
INSERT INTO company _settings ( id , name , full _name , address , phone , email , license _number )
VALUES ( 1 , 'Управляющая компания "Дружба"' , 'О О О "Управляющая компания Дружба"' , 'г . Уфа, ул. Ленина, 1' , '+7 (347) 123-45-67' , 'info@uk-druzhba.ru' , 'Лицензия №12345' )
ON CONFLICT ( id ) DO NOTHING
` );
console . log ( '[dbInit] Таблица company_settings успешно создана' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции настроек компании:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для таблиц досудебной работы
* /
async function applyPreTrialWorkMigration ( client ) {
try {
// Проверяем существование таблицы legal_debtors
const debtorsCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'legal_debtors' `
) ;
if ( debtorsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблицы досудебной работы не найдены, создаем...' ) ;
// Читаем SQL миграцию
const migrationPath = path . join ( _ _dirname , 'migrate_pre_trial_work.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Таблицы досудебной работы успешно созданы' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_pre_trial_work.sql не найден, создаем таблицы вручную...' ) ;
// Создаем таблицы вручную, если файл не найден
await client . query ( `
CREATE TABLE IF NOT EXISTS legal _debtors (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE SET NULL ,
apartment TEXT NOT NULL ,
debtor _name TEXT ,
phone TEXT ,
email TEXT ,
address TEXT NOT NULL ,
debt _amount NUMERIC ( 10 , 2 ) NOT NULL ,
debt _months INTEGER NOT NULL ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'new' CHECK ( status IN ( 'new' , 'in_progress' , 'promised_payment' , 'transferred_to_court' , 'resolved' ) ) ,
transferred _from _finance BOOLEAN DEFAULT FALSE ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_debtors_building ON legal_debtors(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_debtors_status ON legal_debtors(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_debtors_apartment ON legal_debtors(apartment) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_debtors_created ON legal_debtors(created_at DESC) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS pre _trial _work (
id BIGSERIAL PRIMARY KEY ,
debtor _id BIGINT NOT NULL REFERENCES legal _debtors ( id ) ON DELETE CASCADE ,
assigned _to TEXT ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'new' CHECK ( status IN ( 'new' , 'in_progress' , 'promised_payment' , 'transferred_to_court' , 'resolved' ) ) ,
promised _payment _date DATE ,
promised _payment _amount NUMERIC ( 10 , 2 ) ,
transferred _to _court BOOLEAN DEFAULT FALSE ,
court _case _id VARCHAR ( 50 ) ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_work_debtor ON pre_trial_work(debtor_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_work_status ON pre_trial_work(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_work_assigned ON pre_trial_work(assigned_to) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_work_court_case ON pre_trial_work(court_case_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_work_promised_date ON pre_trial_work(promised_payment_date) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS pre _trial _actions (
id BIGSERIAL PRIMARY KEY ,
work _id BIGINT NOT NULL REFERENCES pre _trial _work ( id ) ON DELETE CASCADE ,
action _type VARCHAR ( 20 ) NOT NULL CHECK ( action _type IN ( 'call' , 'letter' , 'visit' ) ) ,
action _date TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
performed _by TEXT NOT NULL ,
result TEXT ,
notes TEXT ,
attachments TEXT [ ] ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_actions_work ON pre_trial_actions(work_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_actions_type ON pre_trial_actions(action_type) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_actions_date ON pre_trial_actions(action_date DESC) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pre_trial_actions_performed ON pre_trial_actions(performed_by) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS promised _payments (
id BIGSERIAL PRIMARY KEY ,
work _id BIGINT NOT NULL REFERENCES pre _trial _work ( id ) ON DELETE CASCADE ,
promised _date DATE NOT NULL ,
promised _amount NUMERIC ( 10 , 2 ) NOT NULL ,
actual _payment _date DATE ,
actual _payment _amount NUMERIC ( 10 , 2 ) ,
is _paid BOOLEAN DEFAULT FALSE ,
reminder _sent BOOLEAN DEFAULT FALSE ,
reminder _date DATE ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_promised_payments_work ON promised_payments(work_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_promised_payments_date ON promised_payments(promised_date) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_promised_payments_paid ON promised_payments(is_paid) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_promised_payments_reminder ON promised_payments(reminder_sent, reminder_date) ` ) ;
console . log ( '[dbInit] Таблицы досудебной работы успешно созданы вручную' ) ;
}
} else {
console . log ( '[dbInit] Таблицы досудебной работы уже существуют' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции досудебной работы:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для таблиц проверок контрагентов
* /
async function applyCounterpartyChecksMigration ( client ) {
try {
// Проверяем существование таблицы counterparty_checks
const checksCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'counterparty_checks' `
) ;
if ( checksCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблицы проверок контрагентов не найдены, создаем...' ) ;
// Читаем SQL миграцию
const migrationPath = path . join ( _ _dirname , 'migrate_counterparty_checks.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Таблицы проверок контрагентов успешно созданы' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_counterparty_checks.sql не найден, создаем таблицы вручную...' ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS counterparty _checks (
id BIGSERIAL PRIMARY KEY ,
inn TEXT NOT NULL ,
kpp TEXT ,
ogrn TEXT ,
name TEXT NOT NULL ,
short _name TEXT ,
type VARCHAR ( 20 ) CHECK ( type IN ( 'LEGAL' , 'INDIVIDUAL' ) ) ,
status VARCHAR ( 20 ) ,
registration _date DATE ,
liquidation _date DATE ,
address TEXT ,
okved TEXT ,
okveds JSONB ,
management _name TEXT ,
management _post TEXT ,
finance _data JSONB ,
authorities _data JSONB ,
phones JSONB ,
emails JSONB ,
employee _count INTEGER ,
risk _level VARCHAR ( 10 ) NOT NULL CHECK ( risk _level IN ( 'low' , 'medium' , 'high' ) ) ,
risk _reasons TEXT [ ] ,
raw _data JSONB ,
checked _by TEXT ,
checked _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_counterparty_checks_inn ON counterparty_checks(inn) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_counterparty_checks_risk_level ON counterparty_checks(risk_level) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_counterparty_checks_checked_at ON counterparty_checks(checked_at DESC) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_counterparty_checks_name ON counterparty_checks(name) ` ) ;
console . log ( '[dbInit] Таблицы проверок контрагентов успешно созданы вручную' ) ;
}
} else {
console . log ( '[dbInit] Таблицы проверок контрагентов уже существуют' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции проверок контрагентов:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции : колонка role в portal _users ( роль пользователя портала )
* /
async function applyPortalUsersRoleMigration ( client ) {
try {
const colCheck = await client . query (
` SELECT column_name FROM information_schema.columns
WHERE table _schema = 'public' AND table _name = 'portal_users' AND column _name = 'role' `
) ;
if ( colCheck . rows . length === 0 ) {
await client . query (
` ALTER TABLE portal_users ADD COLUMN role VARCHAR(30) NOT NULL DEFAULT 'ENGINEER' `
) ;
console . log ( '[dbInit] В portal_users добавлена колонка role' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции portal_users.role:' , error . message ) ;
}
}
/ * *
* Миграция : password _hash и is _active в portal _users для авторизации
* /
async function applyPortalUsersAuthMigration ( client ) {
try {
const cols = await client . query (
` SELECT column_name FROM information_schema.columns
WHERE table _schema = 'public' AND table _name = 'portal_users' AND column _name IN ( 'password_hash' , 'is_active' ) `
) ;
const names = cols . rows . map ( r => r . column _name ) ;
if ( ! names . includes ( 'password_hash' ) ) {
await client . query ( ` ALTER TABLE portal_users ADD COLUMN IF NOT EXISTS password_hash TEXT ` ) ;
console . log ( '[dbInit] В portal_users добавлена колонка password_hash' ) ;
}
if ( ! names . includes ( 'is_active' ) ) {
await client . query ( ` ALTER TABLE portal_users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true ` ) ;
console . log ( '[dbInit] В portal_users добавлена колонка is_active' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции portal_users auth:' , error . message ) ;
}
}
/ * *
* Миграция : поля профиля пользователя и таблица user _preferences
* /
async function applyUserProfileFieldsMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrations' , 'add_user_profile_fields.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
const sql = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( sql ) ;
console . log ( '[dbInit] Миграция add_user_profile_fields.sql применена успешно' ) ;
}
} catch ( error ) {
if ( ! error . message ? . includes ( 'already exists' ) && ! error . message ? . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_user_profile_fields.sql:' , error . message ) ;
}
}
}
async function applyPermissionTemplatesMigration ( client ) {
try {
const tableCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'permission_templates' `
) ;
if ( tableCheck . rows . length === 0 ) {
await client . query ( `
CREATE TABLE IF NOT EXISTS permission _templates (
id BIGSERIAL PRIMARY KEY ,
name TEXT NOT NULL ,
description TEXT ,
permissions JSONB NOT NULL DEFAULT '[]' ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
console . log ( '[dbInit] Таблица permission_templates создана' ) ;
}
const colCheck = await client . query (
` SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'portal_users' AND column_name = 'permissions' `
) ;
if ( colCheck . rows . length === 0 ) {
await client . query ( ` ALTER TABLE portal_users ADD COLUMN IF NOT EXISTS permissions JSONB ` ) ;
console . log ( '[dbInit] В portal_users добавлена колонка permissions' ) ;
}
// scope и for_position в permission_templates; scope в portal_users
const ptCols = await client . query (
` SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'permission_templates' `
) ;
const ptColNames = ( ptCols . rows || [ ] ) . map ( ( r ) => r . column _name ) ;
if ( ! ptColNames . includes ( 'scope' ) ) {
await client . query ( ` ALTER TABLE permission_templates ADD COLUMN IF NOT EXISTS scope VARCHAR(20) NOT NULL DEFAULT 'all' ` ) ;
console . log ( '[dbInit] В permission_templates добавлена колонка scope' ) ;
}
if ( ! ptColNames . includes ( 'for_position' ) ) {
await client . query ( ` ALTER TABLE permission_templates ADD COLUMN IF NOT EXISTS for_position TEXT ` ) ;
console . log ( '[dbInit] В permission_templates добавлена колонка for_position' ) ;
}
if ( ! ptColNames . includes ( 'suggested_role' ) ) {
await client . query ( ` ALTER TABLE permission_templates ADD COLUMN IF NOT EXISTS suggested_role VARCHAR(30) ` ) ;
console . log ( '[dbInit] В permission_templates добавлена колонка suggested_role' ) ;
}
const puScopeCheck = await client . query (
` SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'portal_users' AND column_name = 'scope' `
) ;
if ( puScopeCheck . rows . length === 0 ) {
await client . query ( ` ALTER TABLE portal_users ADD COLUMN IF NOT EXISTS scope VARCHAR(20) NOT NULL DEFAULT 'all' ` ) ;
console . log ( '[dbInit] В portal_users добавлена колонка scope' ) ;
}
// Сид: шаблон «Мастер участка (расширенный)» — участки, заявки, сводка (производство), финансы (счета :own, календарь), офис (закупки, ремонты), PR (мероприятия, фото)
const masterExtendedName = 'Мастер участка (расширенный)' ;
const existingMasterExtended = await client . query (
` SELECT id FROM permission_templates WHERE name = $ 1 ` ,
[ masterExtendedName ]
) ;
if ( existingMasterExtended . rows . length === 0 ) {
const masterExtendedPermissions = [
'objects' ,
'requests' ,
'dashboard_production' ,
'finance_invoices:own' ,
'finance_calendar' ,
'office_supply' ,
'office_repair' ,
'pr_events' ,
'pr_photos' ,
] ;
await client . query (
` INSERT INTO permission_templates (name, description, permissions, scope, for_position, suggested_role)
VALUES ( $1 , $2 , $3 , 'own_district' , $4 , 'MASTER' ) ` ,
[
masterExtendedName ,
'Мастер: участки и заявки по своим участкам; сводка (производство); реестр счетов (только свои), календарь оплат; закупки ТМЦ и ремонт техники; мероприятия и фото отчёты по своим домам.' ,
JSON . stringify ( masterExtendedPermissions ) ,
'Мастер участка' ,
]
) ;
console . log ( '[dbInit] Добавлен шаблон прав: Мастер участка (расширенный)' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции permission_templates:' , error . message ) ;
}
}
/ * *
* Миграция : зоны ответственности — привязка сотрудников к подразделам модулей ( для уведомлений и эффективности ) .
* /
async function applyEmployeeResponsibilityMigration ( client ) {
try {
const check = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'employee_responsibility' `
) ;
if ( check . rows . length === 0 ) {
await client . query ( `
CREATE TABLE employee _responsibility (
id BIGSERIAL PRIMARY KEY ,
employee _id VARCHAR ( 50 ) NOT NULL REFERENCES employees ( id ) ON DELETE CASCADE ,
section VARCHAR ( 50 ) NOT NULL ,
sub _id VARCHAR ( 50 ) NOT NULL ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( employee _id , section , sub _id )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_employee_responsibility_employee ON employee_responsibility(employee_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_employee_responsibility_zone ON employee_responsibility(section, sub_id) ` ) ;
console . log ( '[dbInit] Таблица employee_responsibility создана' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции employee_responsibility:' , error . message ) ;
}
}
/ * *
* Создание администратора портала по умолчанию ( сотрудник iiEasy , логин its ) .
* Выполняется при каждой инициализации : если пользователя с логином 'its' нет — создаётся сотрудник и пользователь .
* /
async function applyDefaultAdminSeed ( client ) {
try {
const existingUser = await client . query (
'SELECT id FROM portal_users WHERE login = $1' ,
[ DEFAULT _ADMIN _LOGIN ]
) ;
if ( existingUser . rows . length > 0 ) {
return ;
}
const employeeExists = await client . query (
'SELECT id FROM employees WHERE id = $1' ,
[ DEFAULT _ADMIN _EMPLOYEE _ID ]
) ;
if ( employeeExists . rows . length === 0 ) {
await client . query (
` INSERT INTO employees (id, name, position, phone, status, salary, assigned_district_id)
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 )
ON CONFLICT ( id ) DO NOTHING ` ,
[
DEFAULT _ADMIN _EMPLOYEE _ID ,
DEFAULT _ADMIN _EMPLOYEE _NAME ,
'Администратор портала' ,
'+70000000000' ,
'active' ,
0 ,
null ,
]
) ;
console . log ( '[dbInit] Создан сотрудник по умолчанию:' , DEFAULT _ADMIN _EMPLOYEE _NAME ) ;
}
const passwordHash = await bcrypt . hash ( DEFAULT _ADMIN _PASSWORD , 10 ) ;
await client . query (
` INSERT INTO portal_users (employee_id, login, role, password_hash, is_active)
VALUES ( $1 , $2 , $3 , $4 , true )
ON CONFLICT ( login ) DO NOTHING ` ,
[ DEFAULT _ADMIN _EMPLOYEE _ID , DEFAULT _ADMIN _LOGIN , DEFAULT _ADMIN _ROLE , passwordHash ]
) ;
const check = await client . query ( 'SELECT id FROM portal_users WHERE login = $1' , [ DEFAULT _ADMIN _LOGIN ] ) ;
if ( check . rows . length > 0 ) {
console . log ( '[dbInit] Создан администратор портала по умолчанию: логин' , DEFAULT _ADMIN _LOGIN ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при создании администратора по умолчанию:' , error . message ) ;
}
}
2026-02-10 11:55:20 +05:00
/ * *
* Демо - пользователь портала ( логин demo , пароль demo123 ) с правами DIRECTOR , как у its .
* /
async function applyDemoUserSeed ( client ) {
try {
const existingUser = await client . query (
'SELECT id FROM portal_users WHERE login = $1' ,
[ DEMO _LOGIN ]
) ;
if ( existingUser . rows . length > 0 ) {
return ;
}
const employeeExists = await client . query (
'SELECT id FROM employees WHERE id = $1' ,
[ DEMO _EMPLOYEE _ID ]
) ;
if ( employeeExists . rows . length === 0 ) {
await client . query (
` INSERT INTO employees (id, name, position, phone, status, salary, assigned_district_id)
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 )
ON CONFLICT ( id ) DO NOTHING ` ,
[
DEMO _EMPLOYEE _ID ,
DEMO _EMPLOYEE _NAME ,
'Демо-пользователь' ,
'+70000000001' ,
'active' ,
0 ,
null ,
]
) ;
console . log ( '[dbInit] Создан сотрудник для демо:' , DEMO _EMPLOYEE _NAME ) ;
}
const passwordHash = await bcrypt . hash ( DEMO _PASSWORD , 10 ) ;
await client . query (
` INSERT INTO portal_users (employee_id, login, role, password_hash, is_active)
VALUES ( $1 , $2 , $3 , $4 , true )
ON CONFLICT ( login ) DO NOTHING ` ,
[ DEMO _EMPLOYEE _ID , DEMO _LOGIN , DEMO _ROLE , passwordHash ]
) ;
const check = await client . query ( 'SELECT id FROM portal_users WHERE login = $1' , [ DEMO _LOGIN ] ) ;
if ( check . rows . length > 0 ) {
console . log ( '[dbInit] Создан демо-пользователь портала: логин' , DEMO _LOGIN , ', роль' , DEMO _ROLE ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при создании демо-пользователя:' , error . message ) ;
}
}
2026-02-04 00:17:04 +05:00
/ * *
* Таблица настроек интеграций ( DaData и др . )
* /
async function applyIntegrationSettingsMigration ( client ) {
try {
const tableCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'integration_settings' `
) ;
if ( tableCheck . rows . length === 0 ) {
await client . query ( `
CREATE TABLE IF NOT EXISTS integration _settings (
id BIGSERIAL PRIMARY KEY ,
key VARCHAR ( 50 ) NOT NULL UNIQUE ,
name TEXT ,
enabled BOOLEAN NOT NULL DEFAULT true ,
config JSONB NOT NULL DEFAULT '{}' ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_integration_settings_key ON integration_settings(key) ` ) ;
await client . query ( `
INSERT INTO integration _settings ( key , name , enabled , config )
VALUES ( 'dadata' , 'DaData (проверка контрагентов по ИНН)' , true , '{"apiKey":"","secret":""}' )
ON CONFLICT ( key ) DO NOTHING
` );
await client . query ( `
INSERT INTO integration _settings ( key , name , enabled , config )
VALUES ( 'ai_chat' , 'ИИ-помощник (чат)' , false , '{"url":"","apiKey":""}' )
ON CONFLICT ( key ) DO NOTHING
` );
await client . query ( `
INSERT INTO integration _settings ( key , name , enabled , config )
VALUES ( 'doma' , 'Дома.АИ (заявки, API)' , true , '{"apiUrl":"","token":""}' )
ON CONFLICT ( key ) DO NOTHING
` );
console . log ( '[dbInit] Таблица integration_settings создана' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции integration_settings:' , error . message ) ;
}
}
/ * *
* Миграция : таблицы безопасности ( логи входа , чёрный список )
* /
async function applySecurityModuleMigration ( client ) {
try {
const authLogsCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'auth_logs' `
) ;
if ( authLogsCheck . rows . length === 0 ) {
await client . query ( `
CREATE TABLE IF NOT EXISTS auth _logs (
id BIGSERIAL PRIMARY KEY ,
ip TEXT NOT NULL ,
login _masked TEXT NOT NULL ,
success BOOLEAN NOT NULL ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_auth_logs_created_at ON auth_logs(created_at DESC) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_auth_logs_success ON auth_logs(success) ` ) ;
console . log ( '[dbInit] Таблица auth_logs создана' ) ;
}
const blacklistCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'security_blacklist' `
) ;
if ( blacklistCheck . rows . length === 0 ) {
await client . query ( `
CREATE TABLE IF NOT EXISTS security _blacklist (
id BIGSERIAL PRIMARY KEY ,
type TEXT NOT NULL CHECK ( type IN ( 'ip' , 'login' ) ) ,
value TEXT NOT NULL ,
reason TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE UNIQUE INDEX IF NOT EXISTS idx_security_blacklist_type_value ON security_blacklist(type, value) ` ) ;
console . log ( '[dbInit] Таблица security_blacklist создана' ) ;
}
const captchaCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'security_captcha' `
) ;
if ( captchaCheck . rows . length === 0 ) {
await client . query ( `
CREATE TABLE IF NOT EXISTS security _captcha (
id SERIAL PRIMARY KEY ,
site _key TEXT ,
secret _key TEXT ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` INSERT INTO security_captcha (id, site_key, secret_key) VALUES (1, NULL, NULL) ON CONFLICT (id) DO NOTHING ` ) ;
console . log ( '[dbInit] Таблица security_captcha создана' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции security:' , error . message ) ;
}
}
/ * *
* Применение миграции для модуля развития
* /
async function applyDevelopmentModuleMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrate_development_module.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию модуля развития из migrate_development_module.sql...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция модуля развития успешно применена' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_development_module.sql не найден, создаем таблицы вручную...' ) ;
// Создаем таблицы модуля развития вручную
await client . query ( `
CREATE TABLE IF NOT EXISTS development _pipeline (
id VARCHAR ( 50 ) PRIMARY KEY ,
address TEXT NOT NULL ,
type VARCHAR ( 10 ) NOT NULL CHECK ( type IN ( 'old' , 'new' ) ) ,
floors INTEGER NOT NULL ,
area NUMERIC ( 10 , 2 ) NOT NULL ,
apartments INTEGER NOT NULL ,
status VARCHAR ( 30 ) NOT NULL CHECK ( status IN ( 'incoming' , 'analysis' , 'agenda_approval' , 'in_person' , 'absentee' , 'protocol_formation' , 'protocol_to_gzhi' , 'gzhi_order' , 'success' , 'failure' ) ) ,
probability INTEGER NOT NULL CHECK ( probability >= 0 AND probability <= 100 ) ,
expected _revenue NUMERIC ( 15 , 2 ) NOT NULL DEFAULT 0 ,
manager TEXT NOT NULL ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE SET NULL ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_pipeline_status ON development_pipeline(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_pipeline_building ON development_pipeline(building_id) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS development _oss _sessions (
id VARCHAR ( 50 ) PRIMARY KEY ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE CASCADE ,
address TEXT NOT NULL ,
start _date DATE NOT NULL ,
end _date DATE NOT NULL ,
quorum _current NUMERIC ( 10 , 2 ) NOT NULL DEFAULT 0 ,
quorum _total NUMERIC ( 10 , 2 ) NOT NULL ,
status VARCHAR ( 20 ) NOT NULL CHECK ( status IN ( 'active' , 'planned' , 'completed' ) ) ,
type VARCHAR ( 20 ) NOT NULL CHECK ( type IN ( 'annual' , 'extraordinary' ) ) ,
description TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_oss_building ON development_oss_sessions(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_oss_status ON development_oss_sessions(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_oss_dates ON development_oss_sessions(start_date, end_date) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS development _audits (
id VARCHAR ( 50 ) PRIMARY KEY ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE CASCADE ,
address TEXT NOT NULL ,
wear _percent INTEGER NOT NULL CHECK ( wear _percent >= 0 AND wear _percent <= 100 ) ,
roof _condition VARCHAR ( 10 ) NOT NULL CHECK ( roof _condition IN ( 'good' , 'poor' , 'fair' ) ) ,
basement _condition VARCHAR ( 10 ) NOT NULL CHECK ( basement _condition IN ( 'good' , 'poor' , 'fair' ) ) ,
calculated _tariff NUMERIC ( 10 , 2 ) NOT NULL ,
projected _margin NUMERIC ( 5 , 2 ) NOT NULL ,
audit _date DATE NOT NULL DEFAULT CURRENT _DATE ,
auditor _name TEXT ,
defect _list _url TEXT ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_audits_building ON development_audits(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_audits_date ON development_audits(audit_date DESC) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS development _marketing _activities (
id VARCHAR ( 50 ) PRIMARY KEY ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE CASCADE ,
address TEXT NOT NULL ,
activists _count INTEGER NOT NULL DEFAULT 0 ,
meetings _held INTEGER NOT NULL DEFAULT 0 ,
ads _distributed INTEGER NOT NULL DEFAULT 0 ,
competitor TEXT ,
status VARCHAR ( 20 ) NOT NULL CHECK ( status IN ( 'voting' , 'my_house' , 'competitor_house' ) ) ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_marketing_building ON development_marketing_activities(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_marketing_status ON development_marketing_activities(status) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS development _building _locations (
id BIGSERIAL PRIMARY KEY ,
building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE CASCADE ,
address TEXT NOT NULL ,
latitude NUMERIC ( 10 , 7 ) ,
longitude NUMERIC ( 10 , 7 ) ,
status VARCHAR ( 20 ) NOT NULL CHECK ( status IN ( 'ours' , 'voting' , 'competitor' ) ) ,
competitor _name TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( building _id )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_locations_building ON development_building_locations(building_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_locations_status ON development_building_locations(status) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS development _oss _registry (
id BIGSERIAL PRIMARY KEY ,
oss _session _id VARCHAR ( 50 ) NOT NULL REFERENCES development _oss _sessions ( id ) ON DELETE CASCADE ,
apartment TEXT NOT NULL ,
owner _name TEXT ,
area NUMERIC ( 10 , 2 ) NOT NULL ,
ballot _submitted BOOLEAN DEFAULT FALSE ,
ballot _date DATE ,
vote _result VARCHAR ( 20 ) CHECK ( vote _result IN ( 'for' , 'against' , 'abstain' ) ) ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_dev_oss_registry_session ON development_oss_registry(oss_session_id) ` ) ;
// Создаем функцию и триггеры для updated_at
await client . query ( `
CREATE OR REPLACE FUNCTION update _updated _at _column ( )
RETURNS TRIGGER AS $$
BEGIN
NEW . updated _at = NOW ( ) ;
RETURN NEW ;
END ;
$$ language 'plpgsql' ;
` );
// Создаем триггеры для каждой таблицы
const tables = [
'development_pipeline' ,
'development_oss_sessions' ,
'development_audits' ,
'development_marketing_activities' ,
'development_building_locations' ,
'development_oss_registry'
] ;
for ( const table of tables ) {
await client . query ( `
DROP TRIGGER IF EXISTS update _$ { table } _updated _at ON $ { table } ;
CREATE TRIGGER update _$ { table } _updated _at BEFORE UPDATE ON $ { table }
FOR EACH ROW EXECUTE FUNCTION update _updated _at _column ( ) ;
` );
}
console . log ( '[dbInit] Таблицы модуля развития успешно созданы вручную' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции модуля развития:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для автоматизации воронки
* /
async function applyPipelineAutomationMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrate_pipeline_automation.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию автоматизации воронки из migrate_pipeline_automation.sql...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция автоматизации воронки успешно применена' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_pipeline_automation.sql не найден, создаем таблицы вручную...' ) ;
// Создаем таблицы автоматизации вручную
await client . query ( `
CREATE TABLE IF NOT EXISTS development _pipeline _history (
id BIGSERIAL PRIMARY KEY ,
pipeline _id VARCHAR ( 50 ) NOT NULL REFERENCES development _pipeline ( id ) ON DELETE CASCADE ,
from _status VARCHAR ( 20 ) ,
to _status VARCHAR ( 20 ) NOT NULL ,
reason TEXT ,
triggered _by VARCHAR ( 50 ) NOT NULL DEFAULT 'manual' ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pipeline_history_pipeline ON development_pipeline_history(pipeline_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pipeline_history_status ON development_pipeline_history(to_status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pipeline_history_date ON development_pipeline_history(created_at DESC) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS development _automation _rules (
id BIGSERIAL PRIMARY KEY ,
rule _name VARCHAR ( 100 ) NOT NULL UNIQUE ,
from _status VARCHAR ( 20 ) ,
to _status VARCHAR ( 20 ) NOT NULL ,
conditions JSONB ,
enabled BOOLEAN DEFAULT TRUE ,
priority INTEGER DEFAULT 0 ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_automation_rules_status ON development_automation_rules(from_status, to_status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_automation_rules_enabled ON development_automation_rules(enabled) ` ) ;
// Добавляем поле last_auto_check если е г о нет
await client . query ( `
DO $$
BEGIN
IF NOT EXISTS ( SELECT 1 FROM information _schema . columns
WHERE table _name = 'development_pipeline' AND column _name = 'last_auto_check' ) THEN
ALTER TABLE development _pipeline ADD COLUMN last _auto _check TIMESTAMPTZ ;
END IF ;
END $$ ;
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_pipeline_last_check ON development_pipeline(last_auto_check) ` ) ;
console . log ( '[dbInit] Таблицы автоматизации воронки успешно созданы вручную' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции автоматизации воронки:' , error . message ) ;
}
}
/ * *
* Применение миграции новых этапов воронки развития ( 9 пунктов )
* Обновляет CHECK по status и мигрирует старые статусы в новые .
* /
async function applyDevelopmentPipelineNewStagesMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrations' , 'development_pipeline_new_stages.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию новых этапов воронки развития...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция новых этапов воронки успешно применена' ) ;
} else {
// Если файла нет — выполняем логику напрямую (для уже существующей БД с о старым CHECK)
const tableCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'development_pipeline' `
) ;
if ( tableCheck . rows . length > 0 ) {
await client . query ( ` ALTER TABLE development_pipeline DROP CONSTRAINT IF EXISTS development_pipeline_status_check ` ) ;
await client . query ( ` UPDATE development_pipeline SET status = 'incoming' WHERE status = 'analysis' ` ) ;
await client . query ( ` UPDATE development_pipeline SET status = 'agenda_approval' WHERE status = 'negotiation' ` ) ;
await client . query ( ` UPDATE development_pipeline SET status = 'in_person' WHERE status = 'preparation' ` ) ;
await client . query ( ` UPDATE development_pipeline SET status = 'absentee' WHERE status = 'voting' ` ) ;
await client . query ( ` UPDATE development_pipeline SET status = 'success' WHERE status = 'transfer' ` ) ;
await client . query ( `
ALTER TABLE development _pipeline ADD CONSTRAINT development _pipeline _status _check
CHECK ( status IN ( 'incoming' , 'analysis' , 'agenda_approval' , 'in_person' , 'absentee' , 'protocol_formation' , 'protocol_to_gzhi' , 'gzhi_order' , 'success' , 'failure' ) )
` );
console . log ( '[dbInit] Новые этапы воронки применены (inline)' ) ;
}
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции новых этапов воронки:' , error . message ) ;
}
}
/ * *
* Добавление этапа « Анализ » ( analysis ) в воронку развития
* /
async function applyDevelopmentPipelineAddAnalysisMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrations' , 'development_pipeline_add_analysis.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию: этап «Анализ» в воронке развития...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Этап «Анализ» успешно добавлен в воронку' ) ;
} else {
const tableCheck = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'development_pipeline' `
) ;
if ( tableCheck . rows . length > 0 ) {
await client . query ( ` ALTER TABLE development_pipeline DROP CONSTRAINT IF EXISTS development_pipeline_status_check ` ) ;
await client . query ( `
ALTER TABLE development _pipeline ADD CONSTRAINT development _pipeline _status _check
CHECK ( status IN ( 'incoming' , 'analysis' , 'agenda_approval' , 'in_person' , 'absentee' , 'protocol_formation' , 'protocol_to_gzhi' , 'gzhi_order' , 'success' , 'failure' ) )
` );
console . log ( '[dbInit] Этап «Анализ» применён (inline)' ) ;
}
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции этапа «Анализ»:' , error . message ) ;
}
}
/ * *
* Миграция аудитов : статусы , индекс сложности , inspection _data
* /
async function applyDevelopmentAuditsStatusAndInspectionMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrations' , 'development_audits_status_and_inspection.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию аудитов (статусы, осмотр)...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция аудитов успешно применена' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции аудитов:' , error . message ) ;
}
}
/ * *
* Миграция О С С : повестка ( agenda _items ) и голоса по пунктам ( votes _by _item )
* /
async function applyDevelopmentOSSAgendaMigration ( client ) {
try {
const migrationPath = path . join ( _ _dirname , 'migrations' , 'development_oss_agenda_and_votes.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
console . log ( '[dbInit] Применяем миграцию О С С (повестка, голоса по пунктам)...' ) ;
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция О С С успешно применена' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции О С С :' , error . message ) ;
}
}
/ * *
* Применение миграции для юридического отдела ( договоры , судебные дела )
* /
async function applyLegalModuleMigration ( client ) {
try {
// Проверяем существование таблицы legal_contracts
const contractsCheck = await client . query (
` SELECT table_name
FROM information _schema . tables
WHERE table _schema = 'public'
AND table _name = 'legal_contracts' `
) ;
if ( contractsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Таблицы юридического отдела не найдены, создаем...' ) ;
// Читаем SQL миграцию
const migrationPath = path . join ( _ _dirname , 'migrate_legal_module.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Таблицы юридического отдела успешно созданы' ) ;
} else {
console . warn ( '[dbInit] Файл migrate_legal_module.sql не найден, создаем таблицы вручную...' ) ;
// Создаем таблицы вручную
await client . query ( `
CREATE TABLE IF NOT EXISTS legal _contracts (
id VARCHAR ( 50 ) PRIMARY KEY ,
number TEXT NOT NULL UNIQUE ,
type TEXT NOT NULL ,
counterparty TEXT NOT NULL ,
counterparty _inn TEXT ,
amount NUMERIC ( 15 , 2 ) NOT NULL ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'draft' CHECK ( status IN ( 'draft' , 'finance_approval' , 'counterparty_approval' , 'signing' , 'active' , 'archived' ) ) ,
start _date DATE NOT NULL ,
end _date DATE NOT NULL ,
auto _prolongation BOOLEAN DEFAULT FALSE ,
manager TEXT NOT NULL ,
has _disagreements BOOLEAN DEFAULT FALSE ,
contract _file _url TEXT ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_contracts_status ON legal_contracts(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_contracts_counterparty ON legal_contracts(counterparty) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_contracts_number ON legal_contracts(number) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS legal _court _cases (
id VARCHAR ( 50 ) PRIMARY KEY ,
case _number TEXT NOT NULL UNIQUE ,
type VARCHAR ( 20 ) NOT NULL CHECK ( type IN ( 'arbitration' , 'civil' , 'debt_recovery' ) ) ,
role VARCHAR ( 20 ) NOT NULL CHECK ( role IN ( 'plaintiff' , 'defendant' ) ) ,
subject TEXT NOT NULL ,
debtor _name TEXT ,
address TEXT ,
amount NUMERIC ( 15 , 2 ) NOT NULL ,
recovered _amount NUMERIC ( 15 , 2 ) DEFAULT 0 ,
amount _at _bailiffs NUMERIC ( 15 , 2 ) DEFAULT 0 ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'pre_trial' CHECK ( status IN ( 'pre_trial' , 'litigation' , 'decision_received' , 'enforcement' , 'closed' ) ) ,
fssp _status TEXT ,
bailiff _name TEXT ,
fssp _last _action _date DATE ,
next _hearing _date DATE ,
judge TEXT ,
court _name TEXT ,
case _file _url TEXT ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_court_cases_status ON legal_court_cases(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_court_cases_type ON legal_court_cases(type) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_court_cases_number ON legal_court_cases(case_number) ` ) ;
await client . query ( `
CREATE TABLE IF NOT EXISTS legal _powers _of _attorney (
id VARCHAR ( 50 ) PRIMARY KEY ,
number TEXT NOT NULL UNIQUE ,
issued _to TEXT NOT NULL ,
issue _date DATE NOT NULL ,
expiry _date DATE NOT NULL ,
status VARCHAR ( 20 ) NOT NULL DEFAULT 'active' CHECK ( status IN ( 'active' , 'expired' , 'revoked' ) ) ,
authority TEXT NOT NULL ,
document _file _url TEXT ,
notes TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_poa_status ON legal_powers_of_attorney(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_legal_poa_expiry ON legal_powers_of_attorney(expiry_date) ` ) ;
console . log ( '[dbInit] Таблицы юридического отдела успешно созданы вручную' ) ;
}
} else {
console . log ( '[dbInit] Таблицы юридического отдела уже существуют' ) ;
}
// Миграция: этапы ФССП и ИП (доп. поля legal_court_cases)
const fsspMigrationPath = path . join ( _ _dirname , 'migrate_legal_fssp_enforcement.sql' ) ;
if ( fs . existsSync ( fsspMigrationPath ) ) {
try {
const fsspSQL = fs . readFileSync ( fsspMigrationPath , 'utf8' ) ;
await client . query ( fsspSQL ) ;
console . log ( '[dbInit] Миграция ФССП/ИП применена' ) ;
} catch ( fsspErr ) {
console . warn ( '[dbInit] Ошибка миграции ФССП (колонки могут уже существовать):' , fsspErr . message ) ;
}
}
const penaltiesMigrationPath = path . join ( _ _dirname , 'migrate_legal_penalties.sql' ) ;
if ( fs . existsSync ( penaltiesMigrationPath ) ) {
try {
const penaltiesSQL = fs . readFileSync ( penaltiesMigrationPath , 'utf8' ) ;
await client . query ( penaltiesSQL ) ;
console . log ( '[dbInit] Миграция пени применена' ) ;
} catch ( penErr ) {
console . warn ( '[dbInit] Ошибка миграции пени:' , penErr . message ) ;
}
}
const caseDocsMigrationPath = path . join ( _ _dirname , 'migrate_legal_case_documents.sql' ) ;
if ( fs . existsSync ( caseDocsMigrationPath ) ) {
try {
const caseDocsSQL = fs . readFileSync ( caseDocsMigrationPath , 'utf8' ) ;
await client . query ( caseDocsSQL ) ;
console . log ( '[dbInit] Миграция документов по делам применена' ) ;
} catch ( docErr ) {
console . warn ( '[dbInit] Ошибка миграции документов по делам:' , docErr . message ) ;
}
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции юридического отдела:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для добавления статуса 'deferred' в enum doma _application _status
* /
async function applyDeferredStatusMigration ( client ) {
try {
// Проверяем, существует ли уже значение 'deferred' в enum
const enumCheck = await client . query ( `
SELECT 1 FROM pg _enum
WHERE enumlabel = 'deferred'
AND enumtypid = ( SELECT oid FROM pg _type WHERE typname = 'doma_application_status' )
` );
if ( enumCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем статус "deferred" в enum doma_application_status...' ) ;
try {
// Пробуем добавить значение (может не работать в транзакции в некоторых версиях PostgreSQL)
await client . query ( ` ALTER TYPE doma_application_status ADD VALUE 'deferred' ` ) ;
console . log ( '[dbInit] Статус "deferred" успешно добавлен в enum doma_application_status' ) ;
} catch ( error ) {
// Если ошибка "already exists" или "duplicate" - это нормально
if ( error . message . includes ( 'already exists' ) ||
error . message . includes ( 'duplicate' ) ||
error . message . includes ( 'уже существует' ) ) {
console . log ( '[dbInit] Статус "deferred" уже существует в enum doma_application_status' ) ;
} else if ( error . message . includes ( 'cannot be executed inside a transaction block' ) ||
error . message . includes ( 'не может быть выполнена внутри блока транзакции' ) ) {
// Если не может быть выполнено в транзакции, используем отдельное подключение
console . log ( '[dbInit] ALTER TYPE не может быть выполнен в транзакции, используем отдельное подключение...' ) ;
const { Pool } = require ( 'pg' ) ;
const tempPool = new Pool ( { connectionString : process . env . DATABASE _URL } ) ;
try {
const tempClient = await tempPool . connect ( ) ;
await tempClient . query ( ` ALTER TYPE doma_application_status ADD VALUE 'deferred' ` ) ;
tempClient . release ( ) ;
console . log ( '[dbInit] Статус "deferred" успешно добавлен через отдельное подключение' ) ;
} catch ( tempError ) {
if ( tempError . message . includes ( 'already exists' ) ||
tempError . message . includes ( 'duplicate' ) ||
tempError . message . includes ( 'уже существует' ) ) {
console . log ( '[dbInit] Статус "deferred" уже существует' ) ;
} else {
console . warn ( '[dbInit] Ошибка при добавлении статуса через отдельное подключение:' , tempError . message ) ;
}
} finally {
await tempPool . end ( ) ;
}
} else {
console . warn ( '[dbInit] Неожиданная ошибка при добавлении статуса "deferred":' , error . message ) ;
}
}
} else {
console . log ( '[dbInit] Статус "deferred" уже существует в enum doma_application_status' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции добавления статуса "deferred":' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для отслеживания производительности ( поля в applications и таблицы статистики )
* /
async function applyDomaMappingsMigration ( client ) {
try {
console . log ( '[dbInit] Применяем миграцию для сопоставлений Doma.AI...' ) ;
const migrationSql = fs . readFileSync (
path . join ( _ _dirname , 'migrate_doma_mappings.sql' ) ,
'utf8'
) ;
await client . query ( migrationSql ) ;
// Создаём функции нормализации, если их нет
await client . query ( `
CREATE OR REPLACE FUNCTION normalize _address ( addr TEXT ) RETURNS TEXT AS $$
BEGIN
RETURN lower ( trim ( regexp _replace ( regexp _replace ( regexp _replace ( regexp _replace (
regexp _replace ( addr , 'г \\.' , 'г ' , 'g' ) ,
'ул\\.' , 'ул ' , 'g' ) ,
'д\\.' , 'д ' , 'g' ) ,
'[.,]' , '' , 'g' ) ,
'\\s+' , ' ' , 'g' ) ) ) ;
END ;
$$ LANGUAGE plpgsql IMMUTABLE ;
` );
await client . query ( `
CREATE OR REPLACE FUNCTION normalize _name ( n TEXT ) RETURNS TEXT AS $$
BEGIN
RETURN lower ( trim ( regexp _replace ( n , '\\s+' , ' ' , 'g' ) ) ) ;
END ;
$$ LANGUAGE plpgsql IMMUTABLE ;
` );
console . log ( '[dbInit] Миграция сопоставлений Doma.AI применена успешно' ) ;
} catch ( err ) {
if ( err . message . includes ( 'already exists' ) || err . message . includes ( 'duplicate' ) ) {
console . log ( '[dbInit] Таблицы сопоставлений Doma.AI уже существуют' ) ;
} else {
console . error ( '[dbInit] Ошибка при применении миграции сопоставлений Doma.AI:' , err . message ) ;
}
}
}
async function applyPerformanceTrackingMigration ( client ) {
try {
// Проверяем наличие колонки building_id
const buildingIdCheck = await client . query ( `
SELECT column _name
FROM information _schema . columns
WHERE table _name = 'applications' AND column _name = 'building_id'
` );
if ( buildingIdCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем поля для отслеживания производительности в таблицу applications...' ) ;
await client . query ( `
ALTER TABLE applications
ADD COLUMN IF NOT EXISTS building _id VARCHAR ( 50 ) REFERENCES buildings ( id ) ON DELETE SET NULL
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_applications_building ON applications(building_id) ` ) ;
console . log ( '[dbInit] Поле building_id добавлено' ) ;
}
// Проверяем наличие колонки employee_id
const employeeIdCheck = await client . query ( `
SELECT column _name
FROM information _schema . columns
WHERE table _name = 'applications' AND column _name = 'employee_id'
` );
if ( employeeIdCheck . rows . length === 0 ) {
await client . query ( `
ALTER TABLE applications
ADD COLUMN IF NOT EXISTS employee _id VARCHAR ( 50 ) REFERENCES employees ( id ) ON DELETE SET NULL
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_applications_employee ON applications(employee_id) ` ) ;
console . log ( '[dbInit] Поле employee_id добавлено' ) ;
}
// Проверяем наличие колонки is_overdue
const overdueCheck = await client . query ( `
SELECT column _name
FROM information _schema . columns
WHERE table _name = 'applications' AND column _name = 'is_overdue'
` );
if ( overdueCheck . rows . length === 0 ) {
await client . query ( `
ALTER TABLE applications
ADD COLUMN IF NOT EXISTS is _overdue BOOLEAN NOT NULL DEFAULT FALSE
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_applications_overdue ON applications(is_overdue) ` ) ;
console . log ( '[dbInit] Поле is_overdue добавлено' ) ;
}
// Проверяем наличие колонки updated_at
const updatedAtCheck = await client . query ( `
SELECT column _name
FROM information _schema . columns
WHERE table _name = 'applications' AND column _name = 'updated_at'
` );
if ( updatedAtCheck . rows . length === 0 ) {
await client . query ( `
ALTER TABLE applications
ADD COLUMN IF NOT EXISTS updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
` );
console . log ( '[dbInit] Поле updated_at добавлено' ) ;
}
// Создаём дополнительные индексы
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_applications_performer ON applications(performer_name) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_applications_deadline ON applications(deadline_at) ` ) ;
// Создаём таблицы для статистики производительности
const employeeStatsCheck = await client . query ( `
SELECT table _name
FROM information _schema . tables
WHERE table _schema = 'public' AND table _name = 'employee_performance_stats'
` );
if ( employeeStatsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаём таблицу employee_performance_stats...' ) ;
await client . query ( `
CREATE TABLE employee _performance _stats (
id BIGSERIAL PRIMARY KEY ,
employee _name TEXT NOT NULL ,
period _start DATE NOT NULL ,
period _end DATE NOT NULL ,
total _assigned INTEGER NOT NULL DEFAULT 0 ,
total _completed INTEGER NOT NULL DEFAULT 0 ,
total _overdue INTEGER NOT NULL DEFAULT 0 ,
total _in _progress INTEGER NOT NULL DEFAULT 0 ,
total _deferred INTEGER NOT NULL DEFAULT 0 ,
completion _rate NUMERIC ( 5 , 2 ) NOT NULL DEFAULT 0 ,
overdue _rate NUMERIC ( 5 , 2 ) NOT NULL DEFAULT 0 ,
performance _score NUMERIC ( 5 , 2 ) NOT NULL DEFAULT 0 ,
district _id VARCHAR ( 50 ) REFERENCES districts ( id ) ON DELETE SET NULL ,
calculated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( employee _name , period _start , period _end )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_performance_stats_employee ON employee_performance_stats(employee_name) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_performance_stats_district ON employee_performance_stats(district_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_performance_stats_period ON employee_performance_stats(period_start, period_end) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_performance_stats_score ON employee_performance_stats(performance_score DESC) ` ) ;
console . log ( '[dbInit] Таблица employee_performance_stats создана' ) ;
}
const districtStatsCheck = await client . query ( `
SELECT table _name
FROM information _schema . tables
WHERE table _schema = 'public' AND table _name = 'district_performance_stats'
` );
if ( districtStatsCheck . rows . length === 0 ) {
console . log ( '[dbInit] Создаём таблицу district_performance_stats...' ) ;
await client . query ( `
CREATE TABLE district _performance _stats (
id BIGSERIAL PRIMARY KEY ,
district _id VARCHAR ( 50 ) NOT NULL REFERENCES districts ( id ) ON DELETE CASCADE ,
period _start DATE NOT NULL ,
period _end DATE NOT NULL ,
total _applications INTEGER NOT NULL DEFAULT 0 ,
total _completed INTEGER NOT NULL DEFAULT 0 ,
total _overdue INTEGER NOT NULL DEFAULT 0 ,
total _in _progress INTEGER NOT NULL DEFAULT 0 ,
completion _rate NUMERIC ( 5 , 2 ) NOT NULL DEFAULT 0 ,
overdue _rate NUMERIC ( 5 , 2 ) NOT NULL DEFAULT 0 ,
average _score NUMERIC ( 5 , 2 ) NOT NULL DEFAULT 0 ,
calculated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
updated _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( ) ,
UNIQUE ( district _id , period _start , period _end )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_district_stats_district ON district_performance_stats(district_id) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_district_stats_period ON district_performance_stats(period_start, period_end) ` ) ;
console . log ( '[dbInit] Таблица district_performance_stats создана' ) ;
}
console . log ( '[dbInit] Миграция отслеживания производительности применена успешно' ) ;
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции отслеживания производительности:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
}
/ * *
* Применение миграции для добавления поля inventory в таблицу districts
* /
async function applyDistrictsInventoryMigration ( client ) {
try {
// Проверяем наличие колонки inventory в таблице districts
const columnCheck = await client . query (
` SELECT column_name
FROM information _schema . columns
WHERE table _schema = 'public'
AND table _name = 'districts'
AND column _name = 'inventory' `
) ;
if ( columnCheck . rows . length === 0 ) {
console . log ( '[dbInit] Добавляем поле inventory в таблицу districts...' ) ;
// Читаем SQL миграцию
const migrationPath = path . join ( _ _dirname , 'migrations' , 'add_inventory_to_districts.sql' ) ;
if ( fs . existsSync ( migrationPath ) ) {
const migrationSQL = fs . readFileSync ( migrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Поле inventory успешно добавлено в districts' ) ;
} else {
// Создаем поле вручную, если файл не найден
await client . query ( `
ALTER TABLE districts
ADD COLUMN IF NOT EXISTS inventory JSONB DEFAULT '[]' : : jsonb
` );
console . log ( '[dbInit] Поле inventory добавлено в districts (вручную)' ) ;
}
} else {
console . log ( '[dbInit] Поле inventory уже существует в districts' ) ;
}
} catch ( error ) {
console . warn ( '[dbInit] Ошибка при применении миграции inventory для districts:' , error . message ) ;
// Н е прерываем выполнение, т.к. это не критично
}
// Миграция: task_id в work_photos (привязка фото отчёта до/после к задаче)
try {
const taskIdMigrationPath = path . join ( _ _dirname , 'migrations' , 'add_task_id_to_work_photos.sql' ) ;
if ( fs . existsSync ( taskIdMigrationPath ) ) {
const migrationSQL = fs . readFileSync ( taskIdMigrationPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция add_task_id_to_work_photos.sql применена успешно' ) ;
}
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_task_id_to_work_photos.sql:' , err . message ) ;
}
}
// Миграция: source и поля для ручной карточки заявки (диспетчерская)
try {
const appManualPath = path . join ( _ _dirname , 'migrations' , 'add_applications_manual_fields.sql' ) ;
if ( fs . existsSync ( appManualPath ) ) {
const migrationSQL = fs . readFileSync ( appManualPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция add_applications_manual_fields.sql применена успешно' ) ;
}
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_applications_manual_fields.sql:' , err . message ) ;
}
}
// Миграция: журнал отключений (outages)
try {
const outagesPath = path . join ( _ _dirname , 'migrations' , 'create_outages.sql' ) ;
if ( fs . existsSync ( outagesPath ) ) {
const migrationSQL = fs . readFileSync ( outagesPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция create_outages.sql применена успешно' ) ;
}
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции create_outages.sql:' , err . message ) ;
}
}
// Миграция: доп. поля отключений (категория, тип работ, resident_message)
try {
const addOutagePath = path . join ( _ _dirname , 'migrations' , 'add_outage_fields.sql' ) ;
if ( fs . existsSync ( addOutagePath ) ) {
const migrationSQL = fs . readFileSync ( addOutagePath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция add_outage_fields.sql применена успешно' ) ;
}
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции add_outage_fields.sql:' , err . message ) ;
}
}
// Миграция: комментарии и история заявок
try {
const commentsHistoryPath = path . join ( _ _dirname , 'migrations' , 'create_application_comments_history.sql' ) ;
if ( fs . existsSync ( commentsHistoryPath ) ) {
const migrationSQL = fs . readFileSync ( commentsHistoryPath , 'utf8' ) ;
await client . query ( migrationSQL ) ;
console . log ( '[dbInit] Миграция create_application_comments_history.sql применена успешно' ) ;
}
} catch ( err ) {
if ( ! err . message . includes ( 'already exists' ) && ! err . message . includes ( 'duplicate' ) ) {
console . warn ( '[dbInit] Ошибка при применении миграции create_application_comments_history.sql:' , err . message ) ;
}
}
}
/ * *
* Миграция : таблицы для ИИ - чата ( диалоги и сообщения по пользователям )
* /
async function applyAIChatMigration ( client ) {
try {
const tableCheck = await client . query ( `
SELECT table _name FROM information _schema . tables
WHERE table _schema = 'public' AND table _name = 'ai_conversations'
` );
if ( tableCheck . rows . length > 0 ) {
console . log ( '[dbInit] Таблицы ИИ-чата уже существуют' ) ;
return ;
}
console . log ( '[dbInit] Создаём таблицы ИИ-чата...' ) ;
await client . query ( `
CREATE TABLE ai _conversations (
id BIGSERIAL PRIMARY KEY ,
user _id BIGINT NOT NULL REFERENCES portal _users ( id ) ON DELETE CASCADE ,
title TEXT ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
) ;
CREATE INDEX IF NOT EXISTS idx _ai _conversations _user ON ai _conversations ( user _id ) ;
CREATE TABLE ai _messages (
id BIGSERIAL PRIMARY KEY ,
conversation _id BIGINT NOT NULL REFERENCES ai _conversations ( id ) ON DELETE CASCADE ,
role TEXT NOT NULL CHECK ( role IN ( 'user' , 'assistant' , 'system' ) ) ,
content TEXT NOT NULL DEFAULT '' ,
tool _calls _json JSONB ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
) ;
CREATE INDEX IF NOT EXISTS idx _ai _messages _conversation ON ai _messages ( conversation _id ) ;
` );
console . log ( '[dbInit] Таблицы ИИ-чата созданы успешно' ) ;
} catch ( err ) {
if ( err . message . includes ( 'already exists' ) || err . message . includes ( 'duplicate' ) ) {
console . log ( '[dbInit] Таблицы ИИ-чата уже существуют' ) ;
} else {
console . error ( '[dbInit] Ошибка миграции ИИ-чата:' , err . message ) ;
throw err ;
}
}
}
/ * *
* Таблица уведомлений для колокольчика ( только для затронутых пользователей )
* /
async function applyNotificationsMigration ( client ) {
try {
const check = await client . query (
` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'notifications' `
) ;
if ( check . rows . length > 0 ) {
console . log ( '[dbInit] Таблица notifications уже существует' ) ;
return ;
}
await client . query ( `
CREATE TABLE notifications (
id BIGSERIAL PRIMARY KEY ,
user _id BIGINT NOT NULL REFERENCES portal _users ( id ) ON DELETE CASCADE ,
type VARCHAR ( 80 ) NOT NULL ,
title TEXT NOT NULL ,
body TEXT ,
entity _type VARCHAR ( 80 ) ,
entity _id TEXT ,
payload JSONB ,
read _at TIMESTAMPTZ ,
created _at TIMESTAMPTZ NOT NULL DEFAULT NOW ( )
)
` );
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_notifications_user_read_created ON notifications(user_id, read_at, created_at DESC) ` ) ;
await client . query ( ` CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id) ` ) ;
console . log ( '[dbInit] Таблица notifications создана' ) ;
} catch ( err ) {
console . warn ( '[dbInit] Ошибка миграции notifications:' , err . message ) ;
}
}
module . exports = { initializeDatabase , checkDatabaseIntegrity } ;