From 9f8ca957c5d1b72d91da509180a5da598ce3bdb2 Mon Sep 17 00:00:00 2001 From: Chris Rosenau Date: Sat, 8 Nov 2025 14:08:41 -0700 Subject: [PATCH] added category deletion --- .../MagentoMigrationController.php | 25 ++ .../MagentoCategoryMigrationService.php | 266 ++++++++++++++++-- resources/views/migration/index.blade.php | 98 ++++++- routes/web.php | 1 + 4 files changed, 361 insertions(+), 29 deletions(-) diff --git a/app/Http/Controllers/MagentoMigrationController.php b/app/Http/Controllers/MagentoMigrationController.php index 43a07b3..53af063 100644 --- a/app/Http/Controllers/MagentoMigrationController.php +++ b/app/Http/Controllers/MagentoMigrationController.php @@ -112,6 +112,31 @@ public function getMagento2CategoryTree() } } + /** + * Delete a category + */ + public function deleteCategory(Request $request, $categoryId) + { + $request->validate([ + 'source' => 'nullable|in:m1,m2', + ]); + + try { + $source = $request->input('source', 'm2'); + $result = $this->migrationService->deleteCategory($categoryId, $source); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Category deletion error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage(), + ], 500); + } + } + /** * Execute the migration */ diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index 0ac843a..6169a77 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -15,6 +15,8 @@ class MagentoCategoryMigrationService protected $storeMapping = []; protected $categoryMapping = []; protected $migrationLog = []; + protected $addedCount = 0; + protected $existingCount = 0; public function __construct() { @@ -310,6 +312,8 @@ public function migrateCategories($storeMapping = []) $this->storeMapping = $storeMapping; $this->migrationLog = []; $this->categoryMapping = []; + $this->addedCount = 0; + $this->existingCount = 0; try { DB::connection($this->magento2Connection)->beginTransaction(); @@ -330,6 +334,9 @@ public function migrateCategories($storeMapping = []) } } + // Update children_count for all affected parent categories + $this->updateChildrenCounts(); + // Migrate category attributes for each store foreach ($this->storeMapping as $m1StoreId => $m2StoreId) { $this->migrateCategoryAttributes($m1StoreId, $m2StoreId); @@ -341,6 +348,8 @@ public function migrateCategories($storeMapping = []) 'success' => true, 'message' => 'Categories migrated successfully', 'migrated_count' => count($this->categoryMapping), + 'added_count' => $this->addedCount, + 'existing_count' => $this->existingCount, 'log' => $this->migrationLog ]; @@ -373,13 +382,64 @@ protected function buildCategoryTree($categories) return $tree; } + /** + * Find existing category in Magento 2 by name and parent + */ + protected function findExistingCategoryInM2($categoryName, $parentId) + { + if (!$categoryName) { + return null; + } + + try { + // Get name attribute ID for M2 + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_category') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return null; + } + + $nameAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + if (!$nameAttributeId) { + return null; + } + + // Find categories with the same name and parent (checking default store view) + $existingCategories = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity', 'e') + ->join($this->magento2Prefix . 'catalog_category_entity_varchar as v', function($join) use ($nameAttributeId) { + $join->on('e.entity_id', '=', 'v.entity_id') + ->where('v.attribute_id', '=', $nameAttributeId) + ->where('v.store_id', '=', 0); + }) + ->where('v.value', '=', $categoryName) + ->where('e.parent_id', '=', $parentId) + ->select('e.entity_id') + ->first(); + + return $existingCategories ? $existingCategories->entity_id : null; + + } catch (Exception $e) { + Log::error("Error finding existing category in M2: " . $e->getMessage()); + return null; + } + } + /** * Migrate a single category */ protected function migrateCategory($m1Category, $parentId = null) { try { - // Check if category already exists + // Check if category already exists in mapping (already processed in this migration) if (isset($this->categoryMapping[$m1Category->entity_id])) { return $this->categoryMapping[$m1Category->entity_id]; } @@ -391,32 +451,46 @@ protected function migrateCategory($m1Category, $parentId = null) $m2ParentId = $this->categoryMapping[$m1Category->parent_id] ?? $parentId; } - // Insert category entity - $m2EntityId = DB::connection($this->magento2Connection) - ->table($this->magento2Prefix . 'catalog_category_entity') - ->insertGetId([ - 'attribute_set_id' => 3, // Default category attribute set - 'parent_id' => $m2ParentId, - 'created_at' => now(), - 'updated_at' => now(), - 'path' => '', - 'position' => $m1Category->position ?? 0, - 'level' => $m1Category->level ?? 1, - 'children_count' => 0, - ]); + // Check if category already exists in M2 database + $existingM2CategoryId = null; + if ($m1Category->name) { + $existingM2CategoryId = $this->findExistingCategoryInM2($m1Category->name, $m2ParentId); + } - // Update path - $path = $this->buildCategoryPath($m2EntityId, $m2ParentId); - DB::connection($this->magento2Connection) - ->table($this->magento2Prefix . 'catalog_category_entity') - ->where('entity_id', $m2EntityId) - ->update(['path' => $path]); + if ($existingM2CategoryId) { + // Category already exists in M2, use existing ID + $m2EntityId = $existingM2CategoryId; + $this->existingCount++; + $this->migrationLog[] = "Found existing category: ID {$m1Category->entity_id} -> {$m2EntityId} ({$m1Category->name})"; + } else { + // Category doesn't exist, create new one + $m2EntityId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity') + ->insertGetId([ + 'attribute_set_id' => 3, // Default category attribute set + 'parent_id' => $m2ParentId, + 'created_at' => now(), + 'updated_at' => now(), + 'path' => '', + 'position' => $m1Category->position ?? 0, + 'level' => $m1Category->level ?? 1, + 'children_count' => 0, + ]); + + // Update path + $path = $this->buildCategoryPath($m2EntityId, $m2ParentId); + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity') + ->where('entity_id', $m2EntityId) + ->update(['path' => $path]); + + $this->addedCount++; + $this->migrationLog[] = "Added new category: ID {$m1Category->entity_id} -> {$m2EntityId} ({$m1Category->name})"; + } // Store mapping $this->categoryMapping[$m1Category->entity_id] = $m2EntityId; - $this->migrationLog[] = "Migrated category ID {$m1Category->entity_id} -> {$m2EntityId} ({$m1Category->name})"; - return $m2EntityId; } catch (Exception $e) { @@ -426,6 +500,40 @@ protected function migrateCategory($m1Category, $parentId = null) } } + /** + * Update children_count for all parent categories + */ + protected function updateChildrenCounts() + { + try { + // Get all unique parent IDs from migrated categories + $parentIds = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity') + ->whereIn('entity_id', array_values($this->categoryMapping)) + ->distinct() + ->pluck('parent_id') + ->toArray(); + + // Update children_count for each parent + foreach ($parentIds as $parentId) { + if ($parentId > 0) { + $childrenCount = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity') + ->where('parent_id', $parentId) + ->count(); + + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity') + ->where('entity_id', $parentId) + ->update(['children_count' => $childrenCount]); + } + } + } catch (Exception $e) { + Log::error("Error updating children counts: " . $e->getMessage()); + // Don't throw, just log the error + } + } + /** * Build category path */ @@ -585,6 +693,120 @@ protected function getMagento2RootCategoryId() return $rootId ?: 2; } + /** + * Delete a category from Magento 2 + * Only allows deletion of root-level categories (parent_id = 0 or 1) with no children + */ + public function deleteCategory($categoryId, $source = 'm2') + { + try { + $connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection; + $prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix; + + // Get category information + $category = DB::connection($connection) + ->table($prefix . 'catalog_category_entity') + ->where('entity_id', $categoryId) + ->first(); + + if (!$category) { + return [ + 'success' => false, + 'message' => 'Category not found', + ]; + } + + // Check if category is root level (parent_id = 0 or 1) + if ($category->parent_id != 0 && $category->parent_id != 1) { + return [ + 'success' => false, + 'message' => 'Only root-level categories (parent_id = 0 or 1) can be deleted', + ]; + } + + // Check if category has children + $childrenCount = DB::connection($connection) + ->table($prefix . 'catalog_category_entity') + ->where('parent_id', $categoryId) + ->count(); + + if ($childrenCount > 0) { + return [ + 'success' => false, + 'message' => 'Cannot delete category with subcategories', + ]; + } + + DB::connection($connection)->beginTransaction(); + + // Get entity type ID + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_category') + ->value('entity_type_id'); + + if ($entityTypeId) { + // Get all attribute IDs for this entity type + $attributeIds = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->pluck('attribute_id') + ->toArray(); + + // Delete attribute values from all attribute tables + $attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime']; + foreach ($attributeTables as $tableType) { + $table = $prefix . 'catalog_category_entity_' . $tableType; + try { + DB::connection($connection) + ->table($table) + ->where('entity_id', $categoryId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + Log::warning("Table {$table} might not exist or has no data: " . $e->getMessage()); + } + } + } + + // Delete the category entity + DB::connection($connection) + ->table($prefix . 'catalog_category_entity') + ->where('entity_id', $categoryId) + ->delete(); + + // Update parent's children_count if parent exists + if ($category->parent_id > 0) { + $parentChildrenCount = DB::connection($connection) + ->table($prefix . 'catalog_category_entity') + ->where('parent_id', $category->parent_id) + ->count(); + + DB::connection($connection) + ->table($prefix . 'catalog_category_entity') + ->where('entity_id', $category->parent_id) + ->update(['children_count' => $parentChildrenCount]); + } + + DB::connection($connection)->commit(); + + return [ + 'success' => true, + 'message' => 'Category deleted successfully', + ]; + + } catch (Exception $e) { + if (isset($connection)) { + DB::connection($connection)->rollBack(); + } + Log::error("Error deleting category {$categoryId}: " . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to delete category: ' . $e->getMessage(), + ]; + } + } + /** * Test database connections */ diff --git a/resources/views/migration/index.blade.php b/resources/views/migration/index.blade.php index efd5ee7..f34a6bc 100644 --- a/resources/views/migration/index.blade.php +++ b/resources/views/migration/index.blade.php @@ -399,6 +399,27 @@ border-radius: 6px; margin: 10px 0; } + + .tree-delete-btn { + background: #dc3545; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + font-size: 0.75em; + cursor: pointer; + margin-left: 8px; + transition: background 0.2s; + } + + .tree-delete-btn:hover { + background: #c82333; + } + + .tree-delete-btn:disabled { + background: #ccc; + cursor: not-allowed; + } @@ -625,7 +646,13 @@ function startMigration() { if (data.success) { addLogEntry(`✓ Migration completed successfully!`, 'success'); - addLogEntry(`Migrated ${data.migrated_count} categories`, 'success'); + addLogEntry(`Total categories processed: ${data.migrated_count}`, 'success'); + if (data.added_count !== undefined) { + addLogEntry(` - Added new categories: ${data.added_count}`, 'success'); + } + if (data.existing_count !== undefined) { + addLogEntry(` - Found existing categories: ${data.existing_count}`, 'success'); + } if (data.log && data.log.length > 0) { data.log.forEach(log => { @@ -695,7 +722,7 @@ function loadM1Tree() { if (data.success) { container.innerHTML = ''; if (data.tree && data.tree.length > 0) { - renderTree(container, data.tree); + renderTree(container, data.tree, 'm1'); } else { container.innerHTML = '
No categories found
'; } @@ -719,7 +746,7 @@ function loadM2Tree() { if (data.success) { container.innerHTML = ''; if (data.tree && data.tree.length > 0) { - renderTree(container, data.tree); + renderTree(container, data.tree, 'm2'); } else { container.innerHTML = '
No categories found
'; } @@ -732,20 +759,64 @@ function loadM2Tree() { }); } + // Delete category + function deleteCategory(categoryId, categoryName, source) { + if (!confirm(`Are you sure you want to delete the category "${categoryName}"?\n\nThis action cannot be undone.`)) { + return; + } + + const container = source === 'm1' ? document.getElementById('m1-tree-container') : document.getElementById('m2-tree-container'); + const originalContent = container.innerHTML; + container.innerHTML = '
Deleting category...
'; + + const url = `{{ route("migration.delete-category", ["categoryId" => ":id"]) }}`.replace(':id', categoryId); + + fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ source: source }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Reload the tree + if (source === 'm1') { + loadM1Tree(); + } else { + loadM2Tree(); + } + alert('Category deleted successfully!'); + } else { + container.innerHTML = originalContent; + alert('Error: ' + (data.message || 'Failed to delete category')); + } + }) + .catch(error => { + container.innerHTML = originalContent; + alert('Error: ' + error.message); + }); + } + // Render tree structure - function renderTree(container, nodes) { + function renderTree(container, nodes, source = 'm2') { nodes.forEach(node => { - const nodeElement = createTreeNode(node); + const nodeElement = createTreeNode(node, source); container.appendChild(nodeElement); }); } // Create a tree node element - function createTreeNode(node) { + function createTreeNode(node, source = 'm2') { const nodeDiv = document.createElement('div'); nodeDiv.className = 'tree-node'; + nodeDiv.setAttribute('data-category-id', node.id); const hasChildren = node.children && node.children.length > 0; + const isRootLevel = node.parent_id == 0 || node.parent_id == 1; + const canDelete = isRootLevel && !hasChildren; const itemDiv = document.createElement('div'); itemDiv.className = 'tree-node-item'; @@ -773,6 +844,19 @@ function createTreeNode(node) { label.appendChild(labelText); label.appendChild(badge); + // Add delete button if category is eligible for deletion + if (canDelete) { + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'tree-delete-btn'; + deleteBtn.textContent = 'Delete'; + deleteBtn.title = 'Delete this category'; + deleteBtn.onclick = function(e) { + e.stopPropagation(); + deleteCategory(node.id, node.name, source); + }; + label.appendChild(deleteBtn); + } + itemDiv.appendChild(toggle); itemDiv.appendChild(label); @@ -782,7 +866,7 @@ function createTreeNode(node) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'tree-children'; node.children.forEach(child => { - childrenDiv.appendChild(createTreeNode(child)); + childrenDiv.appendChild(createTreeNode(child, source)); }); nodeDiv.appendChild(childrenDiv); } diff --git a/routes/web.php b/routes/web.php index 034cf16..ed4bb47 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,4 +14,5 @@ Route::get('/magento1-categories', [MagentoMigrationController::class, 'getMagento1Categories'])->name('migration.magento1-categories'); Route::get('/magento1-category-tree', [MagentoMigrationController::class, 'getMagento1CategoryTree'])->name('migration.magento1-category-tree'); Route::get('/magento2-category-tree', [MagentoMigrationController::class, 'getMagento2CategoryTree'])->name('migration.magento2-category-tree'); + Route::delete('/category/{categoryId}', [MagentoMigrationController::class, 'deleteCategory'])->name('migration.delete-category'); });