diff --git a/app/Http/Controllers/MagentoMigrationController.php b/app/Http/Controllers/MagentoMigrationController.php index 7e2fd22..75310f9 100644 --- a/app/Http/Controllers/MagentoMigrationController.php +++ b/app/Http/Controllers/MagentoMigrationController.php @@ -141,6 +141,33 @@ public function deleteCategory(Request $request, $categoryId) } } + /** + * Rename a Magento 2 category + */ + public function renameCategory(Request $request, $categoryId) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'store_id' => 'nullable|integer', + ]); + + try { + $newName = $request->input('name'); + $storeId = $request->input('store_id', 0); + $result = $this->migrationService->renameCategory($categoryId, $newName, $storeId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Category rename error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Rename failed: ' . $e->getMessage(), + ], 500); + } + } + /** * Execute the migration */ diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index e57b2d8..f87222a 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -780,6 +780,82 @@ protected function getMagento2AttributeIds() return $attributes; } + /** + * Rename a Magento 2 category + */ + public function renameCategory($categoryId, $newName, $storeId = 0) + { + try { + // Validate category exists + $category = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity') + ->where('entity_id', $categoryId) + ->first(); + + if (!$category) { + return [ + 'success' => false, + 'message' => 'Category not found', + ]; + } + + // Prevent renaming system root + if ($categoryId == 0 || $categoryId == 1) { + return [ + 'success' => false, + 'message' => 'Cannot rename system root category', + ]; + } + + // Validate name + $newName = trim($newName); + if (empty($newName)) { + return [ + 'success' => false, + 'message' => 'Category name cannot be empty', + ]; + } + + // Get name attribute ID + $attributeIds = $this->getMagento2AttributeIds(); + if (!isset($attributeIds['name'])) { + return [ + 'success' => false, + 'message' => 'Name attribute not found', + ]; + } + + DB::connection($this->magento2Connection)->beginTransaction(); + + // Update name attribute for the specified store (default store = 0) + $this->insertCategoryAttribute( + $categoryId, + $attributeIds['name'], + $storeId, + $newName + ); + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => 'Category renamed successfully', + 'new_name' => $newName, + ]; + + } catch (Exception $e) { + if (isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error("Error renaming category {$categoryId}: " . $e->getMessage()); + + return [ + 'success' => false, + 'message' => 'Failed to rename category: ' . $e->getMessage(), + ]; + } + } + /** * Get Magento 2 root category ID */ diff --git a/resources/views/migration/index.blade.php b/resources/views/migration/index.blade.php index 780e0d4..8287848 100644 --- a/resources/views/migration/index.blade.php +++ b/resources/views/migration/index.blade.php @@ -420,6 +420,69 @@ background: #ccc; cursor: not-allowed; } + + .tree-rename-btn { + background: #28a745; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + font-size: 0.75em; + cursor: pointer; + margin-left: 8px; + transition: background 0.2s; + } + + .tree-rename-btn:hover { + background: #218838; + } + + .tree-rename-input { + padding: 4px 8px; + border: 2px solid #28a745; + border-radius: 4px; + font-size: 0.9em; + width: 200px; + margin-left: 8px; + } + + .tree-rename-input:focus { + outline: none; + border-color: #218838; + } + + .tree-rename-actions { + display: inline-flex; + gap: 5px; + margin-left: 8px; + } + + .tree-rename-save-btn, .tree-rename-cancel-btn { + padding: 4px 8px; + border: none; + border-radius: 4px; + font-size: 0.75em; + cursor: pointer; + transition: background 0.2s; + } + + .tree-rename-save-btn { + background: #28a745; + color: white; + } + + .tree-rename-save-btn:hover { + background: #218838; + } + + .tree-rename-cancel-btn { + background: #6c757d; + color: white; + } + + .tree-rename-cancel-btn:hover { + background: #5a6268; + } @@ -524,6 +587,20 @@

⚡ Actions

+ +
+

What happens when you click "Start Migration"?

+

The migration process will:

+ +

⚠️ Warning: This will modify your Magento 2 database. Make sure you have a backup before proceeding.

+
+
+
-
-
+ +
+

📋 Migration Logs

+

Detailed logs showing which categories were added, updated, or encountered errors during migration:

+ +
+
+
+ No migration logs yet. Click "Start Migration" to begin. +
+
@@ -664,22 +751,18 @@ function testConnections() { } function previewCategories() { - fetch('{{ route("migration.magento1-categories") }}') - .then(response => response.json()) - .then(data => { - if (data.success) { - let message = `Found ${data.count} categories in Magento 1.\n\nFirst 10 categories:\n\n`; - data.categories.forEach(cat => { - message += `ID: ${cat.id} - ${cat.name} (Level: ${cat.level})\n`; - }); - alert(message); - } else { - alert('Failed to fetch categories'); - } - }) - .catch(error => { - alert('Error: ' + error.message); - }); + // Find the Category Trees tab button and switch to it + const tabs = document.querySelectorAll('.tab'); + let categoriesTab = null; + tabs.forEach(tab => { + if (tab.textContent.trim() === 'Category Trees') { + categoriesTab = tab; + } + }); + + if (categoriesTab) { + switchTab('categories', categoriesTab); + } } function startMigration() { @@ -705,8 +788,13 @@ function startMigration() { document.getElementById('migrateBtn').disabled = true; document.getElementById('loading').classList.add('active'); - document.getElementById('logContainer').classList.add('active'); - document.getElementById('logContent').innerHTML = '
Starting migration...
'; + + // Clear and initialize migration logs + const migrationLogContent = document.getElementById('migrationLogContent'); + migrationLogContent.innerHTML = '
Starting migration...
'; + + // Scroll to logs section + document.getElementById('migrationLogContainer').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); fetch('{{ route("migration.migrate") }}', { method: 'POST', @@ -754,12 +842,25 @@ function startMigration() { } function addLogEntry(message, type = '') { - const logContent = document.getElementById('logContent'); + // Add to migration logs section (primary) + const migrationLogContent = document.getElementById('migrationLogContent'); const entry = document.createElement('div'); entry.className = `log-entry ${type}`; entry.textContent = message; - logContent.appendChild(entry); - logContent.scrollTop = logContent.scrollHeight; + migrationLogContent.appendChild(entry); + + // Auto-scroll to bottom + const migrationLogContainer = document.getElementById('migrationLogContainer'); + migrationLogContainer.scrollTop = migrationLogContainer.scrollHeight; + + // Also add to old log container if it exists (for backward compatibility) + const oldLogContent = document.getElementById('logContent'); + if (oldLogContent) { + const oldEntry = document.createElement('div'); + oldEntry.className = `log-entry ${type}`; + oldEntry.textContent = message; + oldLogContent.appendChild(oldEntry); + } } // Tab switching @@ -918,6 +1019,19 @@ function createTreeNode(node, source = 'm2') { label.appendChild(labelText); label.appendChild(badge); + // Add rename button for Magento 2 categories only (except system root) + if (canDelete && source === 'm2') { + const renameBtn = document.createElement('button'); + renameBtn.className = 'tree-rename-btn'; + renameBtn.textContent = 'Rename'; + renameBtn.title = 'Rename this category'; + renameBtn.onclick = function(e) { + e.stopPropagation(); + startRenameCategory(nodeDiv, node, source); + }; + label.appendChild(renameBtn); + } + // Add delete button for all categories (except system root) if (canDelete) { const deleteBtn = document.createElement('button'); @@ -972,6 +1086,160 @@ function toggleNode(toggleElement) { } } } + + // Start renaming a category + function startRenameCategory(nodeDiv, node, source) { + const label = nodeDiv.querySelector('.tree-label'); + const labelText = label.querySelector('.tree-label-text'); + const originalName = node.name || 'Unnamed Category'; + + // Hide the label text and badge + labelText.style.display = 'none'; + const badge = label.querySelector('.tree-badge'); + if (badge) badge.style.display = 'none'; + + // Hide rename and delete buttons + const renameBtn = label.querySelector('.tree-rename-btn'); + const deleteBtn = label.querySelector('.tree-delete-btn'); + if (renameBtn) renameBtn.style.display = 'none'; + if (deleteBtn) deleteBtn.style.display = 'none'; + + // Create input field + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'tree-rename-input'; + input.value = originalName; + input.onclick = function(e) { + e.stopPropagation(); + }; + + // Create action buttons + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'tree-rename-actions'; + + const saveBtn = document.createElement('button'); + saveBtn.className = 'tree-rename-save-btn'; + saveBtn.textContent = 'Save'; + saveBtn.onclick = function(e) { + e.stopPropagation(); + saveRenameCategory(nodeDiv, node, input.value, source); + }; + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'tree-rename-cancel-btn'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.onclick = function(e) { + e.stopPropagation(); + cancelRenameCategory(nodeDiv, labelText, badge, renameBtn, deleteBtn); + }; + + actionsDiv.appendChild(saveBtn); + actionsDiv.appendChild(cancelBtn); + + label.appendChild(input); + label.appendChild(actionsDiv); + + // Focus and select input + input.focus(); + input.select(); + + // Handle Enter and Escape keys + input.onkeydown = function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + saveRenameCategory(nodeDiv, node, input.value, source); + } else if (e.key === 'Escape') { + e.preventDefault(); + cancelRenameCategory(nodeDiv, labelText, badge, renameBtn, deleteBtn); + } + }; + } + + // Save category rename + function saveRenameCategory(nodeDiv, node, newName, source) { + const categoryId = node.id; + const label = nodeDiv.querySelector('.tree-label'); + const input = label.querySelector('.tree-rename-input'); + const actionsDiv = label.querySelector('.tree-rename-actions'); + + if (!newName || newName.trim() === '') { + alert('Category name cannot be empty'); + return; + } + + // Disable input and buttons + input.disabled = true; + const saveBtn = actionsDiv.querySelector('.tree-rename-save-btn'); + const cancelBtn = actionsDiv.querySelector('.tree-rename-cancel-btn'); + if (saveBtn) saveBtn.disabled = true; + if (cancelBtn) cancelBtn.disabled = true; + + const url = `{{ route("migration.rename-category", ["categoryId" => ":id"]) }}`.replace(':id', categoryId); + + fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ name: newName.trim() }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Update the node name + node.name = data.new_name; + + // Update the label text + const labelText = label.querySelector('.tree-label-text'); + const productCount = node.product_count !== undefined ? node.product_count : 0; + labelText.textContent = `${data.new_name} (${productCount})`; + + // Restore display + labelText.style.display = ''; + const badge = label.querySelector('.tree-badge'); + if (badge) badge.style.display = ''; + const renameBtn = label.querySelector('.tree-rename-btn'); + const deleteBtn = label.querySelector('.tree-delete-btn'); + if (renameBtn) renameBtn.style.display = ''; + if (deleteBtn) deleteBtn.style.display = ''; + + // Remove input and actions + label.removeChild(input); + label.removeChild(actionsDiv); + } else { + alert('Error: ' + (data.message || 'Failed to rename category')); + // Re-enable input and buttons + input.disabled = false; + if (saveBtn) saveBtn.disabled = false; + if (cancelBtn) cancelBtn.disabled = false; + } + }) + .catch(error => { + alert('Error: ' + error.message); + // Re-enable input and buttons + input.disabled = false; + if (saveBtn) saveBtn.disabled = false; + if (cancelBtn) cancelBtn.disabled = false; + }); + } + + // Cancel category rename + function cancelRenameCategory(nodeDiv, labelText, badge, renameBtn, deleteBtn) { + const label = nodeDiv.querySelector('.tree-label'); + const input = label.querySelector('.tree-rename-input'); + const actionsDiv = label.querySelector('.tree-rename-actions'); + + // Restore display + labelText.style.display = ''; + if (badge) badge.style.display = ''; + if (renameBtn) renameBtn.style.display = ''; + if (deleteBtn) deleteBtn.style.display = ''; + + // Remove input and actions + if (input) label.removeChild(input); + if (actionsDiv) label.removeChild(actionsDiv); + } diff --git a/routes/web.php b/routes/web.php index ed4bb47..4125db1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,4 +15,5 @@ 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'); + Route::put('/category/{categoryId}/rename', [MagentoMigrationController::class, 'renameCategory'])->name('migration.rename-category'); });