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:
+
+ - Migrate category structure: Create categories in Magento 2 based on the hierarchy from Magento 1, preserving parent-child relationships and positions
+ - Migrate category attributes: For each mapped store, migrate category names, URL keys, and active status from Magento 1 to Magento 2
+ - Preserve hierarchy: Maintain the exact category tree structure with proper levels and paths
+ - Handle existing categories: If a category with the same name and parent already exists in Magento 2, it will be reused instead of creating a duplicate
+ - Generate logs: Provide detailed migration logs showing which categories were added, updated, or encountered errors
+
+
⚠️ 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);
+ }