added category deletion

This commit is contained in:
Chris Rosenau 2025-11-08 14:08:41 -07:00
parent 44c7c7354a
commit 9f8ca957c5
4 changed files with 361 additions and 29 deletions

View File

@ -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 * Execute the migration
*/ */

View File

@ -15,6 +15,8 @@ class MagentoCategoryMigrationService
protected $storeMapping = []; protected $storeMapping = [];
protected $categoryMapping = []; protected $categoryMapping = [];
protected $migrationLog = []; protected $migrationLog = [];
protected $addedCount = 0;
protected $existingCount = 0;
public function __construct() public function __construct()
{ {
@ -310,6 +312,8 @@ public function migrateCategories($storeMapping = [])
$this->storeMapping = $storeMapping; $this->storeMapping = $storeMapping;
$this->migrationLog = []; $this->migrationLog = [];
$this->categoryMapping = []; $this->categoryMapping = [];
$this->addedCount = 0;
$this->existingCount = 0;
try { try {
DB::connection($this->magento2Connection)->beginTransaction(); 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 // Migrate category attributes for each store
foreach ($this->storeMapping as $m1StoreId => $m2StoreId) { foreach ($this->storeMapping as $m1StoreId => $m2StoreId) {
$this->migrateCategoryAttributes($m1StoreId, $m2StoreId); $this->migrateCategoryAttributes($m1StoreId, $m2StoreId);
@ -341,6 +348,8 @@ public function migrateCategories($storeMapping = [])
'success' => true, 'success' => true,
'message' => 'Categories migrated successfully', 'message' => 'Categories migrated successfully',
'migrated_count' => count($this->categoryMapping), 'migrated_count' => count($this->categoryMapping),
'added_count' => $this->addedCount,
'existing_count' => $this->existingCount,
'log' => $this->migrationLog 'log' => $this->migrationLog
]; ];
@ -373,13 +382,64 @@ protected function buildCategoryTree($categories)
return $tree; 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 * Migrate a single category
*/ */
protected function migrateCategory($m1Category, $parentId = null) protected function migrateCategory($m1Category, $parentId = null)
{ {
try { 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])) { if (isset($this->categoryMapping[$m1Category->entity_id])) {
return $this->categoryMapping[$m1Category->entity_id]; return $this->categoryMapping[$m1Category->entity_id];
} }
@ -391,7 +451,19 @@ protected function migrateCategory($m1Category, $parentId = null)
$m2ParentId = $this->categoryMapping[$m1Category->parent_id] ?? $parentId; $m2ParentId = $this->categoryMapping[$m1Category->parent_id] ?? $parentId;
} }
// Insert category entity // Check if category already exists in M2 database
$existingM2CategoryId = null;
if ($m1Category->name) {
$existingM2CategoryId = $this->findExistingCategoryInM2($m1Category->name, $m2ParentId);
}
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) $m2EntityId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_entity') ->table($this->magento2Prefix . 'catalog_category_entity')
->insertGetId([ ->insertGetId([
@ -412,11 +484,13 @@ protected function migrateCategory($m1Category, $parentId = null)
->where('entity_id', $m2EntityId) ->where('entity_id', $m2EntityId)
->update(['path' => $path]); ->update(['path' => $path]);
$this->addedCount++;
$this->migrationLog[] = "Added new category: ID {$m1Category->entity_id} -> {$m2EntityId} ({$m1Category->name})";
}
// Store mapping // Store mapping
$this->categoryMapping[$m1Category->entity_id] = $m2EntityId; $this->categoryMapping[$m1Category->entity_id] = $m2EntityId;
$this->migrationLog[] = "Migrated category ID {$m1Category->entity_id} -> {$m2EntityId} ({$m1Category->name})";
return $m2EntityId; return $m2EntityId;
} catch (Exception $e) { } 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 * Build category path
*/ */
@ -585,6 +693,120 @@ protected function getMagento2RootCategoryId()
return $rootId ?: 2; 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 * Test database connections
*/ */

View File

@ -399,6 +399,27 @@
border-radius: 6px; border-radius: 6px;
margin: 10px 0; 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;
}
</style> </style>
</head> </head>
<body> <body>
@ -625,7 +646,13 @@ function startMigration() {
if (data.success) { if (data.success) {
addLogEntry(`✓ Migration completed successfully!`, '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) { if (data.log && data.log.length > 0) {
data.log.forEach(log => { data.log.forEach(log => {
@ -695,7 +722,7 @@ function loadM1Tree() {
if (data.success) { if (data.success) {
container.innerHTML = ''; container.innerHTML = '';
if (data.tree && data.tree.length > 0) { if (data.tree && data.tree.length > 0) {
renderTree(container, data.tree); renderTree(container, data.tree, 'm1');
} else { } else {
container.innerHTML = '<div class="tree-loading">No categories found</div>'; container.innerHTML = '<div class="tree-loading">No categories found</div>';
} }
@ -719,7 +746,7 @@ function loadM2Tree() {
if (data.success) { if (data.success) {
container.innerHTML = ''; container.innerHTML = '';
if (data.tree && data.tree.length > 0) { if (data.tree && data.tree.length > 0) {
renderTree(container, data.tree); renderTree(container, data.tree, 'm2');
} else { } else {
container.innerHTML = '<div class="tree-loading">No categories found</div>'; container.innerHTML = '<div class="tree-loading">No categories found</div>';
} }
@ -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 = '<div class="tree-loading">Deleting category...</div>';
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 // Render tree structure
function renderTree(container, nodes) { function renderTree(container, nodes, source = 'm2') {
nodes.forEach(node => { nodes.forEach(node => {
const nodeElement = createTreeNode(node); const nodeElement = createTreeNode(node, source);
container.appendChild(nodeElement); container.appendChild(nodeElement);
}); });
} }
// Create a tree node element // Create a tree node element
function createTreeNode(node) { function createTreeNode(node, source = 'm2') {
const nodeDiv = document.createElement('div'); const nodeDiv = document.createElement('div');
nodeDiv.className = 'tree-node'; nodeDiv.className = 'tree-node';
nodeDiv.setAttribute('data-category-id', node.id);
const hasChildren = node.children && node.children.length > 0; 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'); const itemDiv = document.createElement('div');
itemDiv.className = 'tree-node-item'; itemDiv.className = 'tree-node-item';
@ -773,6 +844,19 @@ function createTreeNode(node) {
label.appendChild(labelText); label.appendChild(labelText);
label.appendChild(badge); 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(toggle);
itemDiv.appendChild(label); itemDiv.appendChild(label);
@ -782,7 +866,7 @@ function createTreeNode(node) {
const childrenDiv = document.createElement('div'); const childrenDiv = document.createElement('div');
childrenDiv.className = 'tree-children'; childrenDiv.className = 'tree-children';
node.children.forEach(child => { node.children.forEach(child => {
childrenDiv.appendChild(createTreeNode(child)); childrenDiv.appendChild(createTreeNode(child, source));
}); });
nodeDiv.appendChild(childrenDiv); nodeDiv.appendChild(childrenDiv);
} }

View File

@ -14,4 +14,5 @@
Route::get('/magento1-categories', [MagentoMigrationController::class, 'getMagento1Categories'])->name('migration.magento1-categories'); 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('/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::get('/magento2-category-tree', [MagentoMigrationController::class, 'getMagento2CategoryTree'])->name('migration.magento2-category-tree');
Route::delete('/category/{categoryId}', [MagentoMigrationController::class, 'deleteCategory'])->name('migration.delete-category');
}); });