tableExists($table) && !$this->fieldExists($table, $column)) { $this->db->query("ALTER TABLE `{$table}` ADD `{$column}` {$definition}"); } } private function hasIndex($table, $name) { if (!$this->tableExists($table)) return false; foreach ($this->db->query("SHOW INDEX FROM `{$table}`")->result() as $index) { if ($index->Key_name === $name) return true; } return false; } private function tableExists($table) { return (bool) $this->db->query( 'SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=? LIMIT 1', array($table) )->row(); } private function fieldExists($table, $column) { return (bool) $this->db->query( 'SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=? AND column_name=? LIMIT 1', array($table, $column) )->row(); } public function up() { foreach (array( 'company_id' => 'BIGINT UNSIGNED NULL AFTER `id`', 'description' => 'TEXT NULL AFTER `control_mode`', 'revision_no' => 'INT NOT NULL DEFAULT 1 AFTER `version_no`', 'parent_budget_id' => 'BIGINT UNSIGNED NULL AFTER `revision_no`', 'submitted_by' => 'INT NULL AFTER `created_by`', 'submitted_at' => 'DATETIME NULL AFTER `submitted_by`', 'rejected_by' => 'INT NULL AFTER `approved_at`', 'rejected_at' => 'DATETIME NULL AFTER `rejected_by`', 'rejection_reason' => 'TEXT NULL AFTER `rejected_at`', 'updated_by' => 'INT NULL AFTER `created_at`', 'updated_at' => 'DATETIME NULL AFTER `updated_by`' ) as $column => $definition) $this->column('budgets', $column, $definition); foreach (array( 'company_id' => 'BIGINT UNSIGNED NULL AFTER `id`', 'salesperson_id' => 'BIGINT UNSIGNED NULL AFTER `profit_center_id`', 'updated_by' => 'INT NULL AFTER `notes`', 'created_at' => 'DATETIME NULL AFTER `updated_by`', 'updated_at' => 'DATETIME NULL AFTER `created_at`' ) as $column => $definition) $this->column('budget_lines', $column, $definition); foreach (array( 'company_id' => 'BIGINT UNSIGNED NULL AFTER `id`', 'description' => 'VARCHAR(255) NULL AFTER `name`', 'source_type' => 'VARCHAR(40) NULL AFTER `parent_id`', 'source_id' => 'BIGINT UNSIGNED NULL AFTER `source_type`', 'created_by' => 'INT NULL AFTER `is_active`', 'updated_by' => 'INT NULL AFTER `created_by`', 'updated_at' => 'DATETIME NULL AFTER `created_at`' ) as $column => $definition) $this->column('business_dimensions', $column, $definition); foreach (array('require_branch','require_department','require_project','require_cost_center','require_profit_center') as $column) { $this->column('accounts', $column, 'TINYINT(1) NOT NULL DEFAULT 0 AFTER `is_hidden`'); } foreach (array('branch_id','department_id','profit_center_id','salesperson_id') as $column) { $this->column('purchase_order_lines', $column, 'BIGINT UNSIGNED NULL AFTER `budget_account_id`'); } if (!$this->tableExists('budget_commitments')) { $this->db->query("CREATE TABLE `budget_commitments` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `company_id` BIGINT UNSIGNED NOT NULL, `source_type` VARCHAR(50) NOT NULL, `source_id` BIGINT UNSIGNED NOT NULL, `source_line_id` BIGINT UNSIGNED NULL, `account_id` INT NOT NULL, `period` CHAR(7) NOT NULL, `branch_id` BIGINT UNSIGNED NULL, `department_id` BIGINT UNSIGNED NULL, `project_id` BIGINT UNSIGNED NULL, `cost_center_id` BIGINT UNSIGNED NULL, `profit_center_id` BIGINT UNSIGNED NULL, `salesperson_id` BIGINT UNSIGNED NULL, `original_amount` DECIMAL(18,2) NOT NULL DEFAULT 0, `realized_amount` DECIMAL(18,2) NOT NULL DEFAULT 0, `released_amount` DECIMAL(18,2) NOT NULL DEFAULT 0, `status` ENUM('active','partially_realized','realized','released') NOT NULL DEFAULT 'active', `created_at` DATETIME NOT NULL, `updated_at` DATETIME NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_budget_commitment_source` (`company_id`,`source_type`,`source_line_id`), KEY `idx_budget_commitment_report` (`company_id`,`period`,`account_id`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } if (!$this->tableExists('budget_audit_logs')) { $this->db->query("CREATE TABLE `budget_audit_logs` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `company_id` BIGINT UNSIGNED NOT NULL, `budget_id` BIGINT UNSIGNED NULL, `entity_type` VARCHAR(40) NOT NULL, `entity_id` BIGINT UNSIGNED NULL, `action` VARCHAR(50) NOT NULL, `old_values` LONGTEXT NULL, `new_values` LONGTEXT NULL, `reason` TEXT NULL, `created_by` INT NULL, `created_at` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_budget_audit_entity` (`company_id`,`entity_type`,`entity_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } if (!$this->hasIndex('budgets', 'idx_budget_company_year_status')) $this->db->query('ALTER TABLE `budgets` ADD KEY `idx_budget_company_year_status` (`company_id`,`fiscal_year`,`status`)'); if (!$this->hasIndex('budget_lines', 'idx_budget_line_report')) $this->db->query('ALTER TABLE `budget_lines` ADD KEY `idx_budget_line_report` (`company_id`,`period`,`account_id`)'); if (!$this->hasIndex('business_dimensions', 'idx_dimension_lookup')) $this->db->query('ALTER TABLE `business_dimensions` ADD KEY `idx_dimension_lookup` (`company_id`,`dimension_type`,`is_active`,`name`)'); $this->db->query('UPDATE `budgets` SET `revision_no`=`version_no` WHERE `revision_no`=1 AND `version_no`>1'); if ($this->tableExists('k_departments')) { $this->db->query("UPDATE business_dimensions bd JOIN k_departments d ON bd.dimension_type='department' AND bd.code=CONCAT('DEP-',d.id) SET bd.source_type='k_departments',bd.source_id=d.id,bd.name=d.department_name,bd.description=d.description WHERE bd.source_id IS NULL"); } } public function down() { throw new RuntimeException('Gunakan backup database untuk rollback finalisasi Budget & Dimensi.'); } }