From 353f9e0564827ca7c4b1066ad3c695294c4724ca Mon Sep 17 00:00:00 2001 From: Chris Rosenau Date: Thu, 11 Dec 2025 23:07:37 -0700 Subject: [PATCH] updated customers --- app/Http/Controllers/CustomersController.php | 62 ++++- .../MagentoCategoryMigrationService.php | 59 ++++- .../MagentoProductMigrationService.php | 35 ++- ...7-3161ad9d-3ff4-4a65-80fc-7e67b3e01c72.png | 0 ...e-691a52f0-582a-4795-918b-7378da7c637d.png | 0 resources/js/customers.js | 225 +++++++++++++++--- resources/views/customers/index.blade.php | 27 ++- routes/web.php | 1 + 8 files changed, 369 insertions(+), 40 deletions(-) create mode 100644 assets/Screenshot_From_2025-11-30_22-33-37-3161ad9d-3ff4-4a65-80fc-7e67b3e01c72.png create mode 100644 assets/image-691a52f0-582a-4795-918b-7378da7c637d.png diff --git a/app/Http/Controllers/CustomersController.php b/app/Http/Controllers/CustomersController.php index b5587db..b95bec3 100644 --- a/app/Http/Controllers/CustomersController.php +++ b/app/Http/Controllers/CustomersController.php @@ -5,6 +5,7 @@ use App\Services\MagentoCategoryMigrationService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Cache; class CustomersController extends Controller { @@ -39,13 +40,30 @@ public function index() public function migrateCustomers(Request $request) { try { - $dryRun = $request->input('dry_run', false); - $result = $this->migrationService->migrateCustomers($dryRun); + // Handle both JSON and form data + $dryRun = false; + $progressKey = null; + + if ($request->isJson()) { + $dryRun = $request->json()->get('dry_run', false); + $progressKey = $request->json()->get('progress_key', null); + } else { + $dryRun = $request->input('dry_run', false); + $progressKey = $request->input('progress_key', null); + } + + // Convert string "true"/"false" to boolean if needed + if (is_string($dryRun)) { + $dryRun = filter_var($dryRun, FILTER_VALIDATE_BOOLEAN); + } + + $result = $this->migrationService->migrateCustomers($dryRun, $progressKey); return response()->json($result, $result['success'] ? 200 : 400); } catch (\Exception $e) { Log::error('Customer migration error: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); return response()->json([ 'success' => false, @@ -58,6 +76,46 @@ public function migrateCustomers(Request $request) } } + /** + * Get customer migration progress + */ + public function getMigrationProgress(Request $request) + { + try { + $progressKey = $request->input('progress_key'); + + if (empty($progressKey)) { + return response()->json([ + 'success' => false, + 'message' => 'Progress key is required' + ], 400); + } + + $progress = Cache::get($progressKey); + + if (!$progress) { + return response()->json([ + 'success' => false, + 'message' => 'Progress not found', + 'progress' => null + ], 404); + } + + return response()->json([ + 'success' => true, + 'progress' => $progress + ]); + + } catch (\Exception $e) { + Log::error('Get customer migration progress error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to get progress: ' . $e->getMessage() + ], 500); + } + } + /** * Delete a single customer from Magento 2 */ diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index eed59ae..dc41551 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Cache; use Exception; class MagentoCategoryMigrationService @@ -5678,7 +5679,7 @@ public function getM2CustomersNotInM1() /** * Migrate all customers from Magento 1 to Magento 2 */ - public function migrateCustomers($dryRun = false) + public function migrateCustomers($dryRun = false, $progressKey = null) { try { $this->migrationLog = []; @@ -5710,6 +5711,20 @@ public function migrateCustomers($dryRun = false) // Get all M1 customers $m1Customers = $this->getMagento1Customers(); + $totalCustomers = $m1Customers->count(); + + // Initialize progress tracking + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => $totalCustomers, + 'current' => 0, + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'status' => 'running', + 'current_email' => '' + ], 3600); + } // Get all customer attribute IDs from M1 $m1AttributeIds = DB::connection($this->magento1Connection) @@ -5729,10 +5744,25 @@ public function migrateCustomers($dryRun = false) DB::connection($this->magento2Connection)->beginTransaction(); } + $currentIndex = 0; foreach ($m1Customers as $m1Customer) { + $currentIndex++; try { $m1Email = !empty($m1Customer->email) ? strtolower(trim($m1Customer->email)) : null; + // Update progress if tracking enabled + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => $totalCustomers, + 'current' => $currentIndex, + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'status' => 'running', + 'current_email' => $m1Email ?? 'N/A' + ], 3600); + } + if (empty($m1Email)) { $this->migrationLog[] = "SKIPPED: Customer ID {$m1Customer->entity_id} - no email address"; continue; @@ -5844,6 +5874,19 @@ public function migrateCustomers($dryRun = false) if (!$dryRun) { DB::connection($this->magento2Connection)->commit(); } + + // Update progress to completed + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => $totalCustomers, + 'current' => $totalCustomers, + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'status' => 'completed', + 'current_email' => '' + ], 3600); + } return [ 'success' => true, @@ -5858,6 +5901,20 @@ public function migrateCustomers($dryRun = false) if (!$dryRun) { DB::connection($this->magento2Connection)->rollBack(); } + + // Update progress to failed + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => isset($totalCustomers) ? $totalCustomers : 0, + 'current' => isset($currentIndex) ? $currentIndex : 0, + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'status' => 'failed', + 'current_email' => '' + ], 3600); + } + Log::error('Error migrating customers: ' . $e->getMessage()); return [ 'success' => false, diff --git a/app/Services/MagentoProductMigrationService.php b/app/Services/MagentoProductMigrationService.php index c827527..ae657dc 100644 --- a/app/Services/MagentoProductMigrationService.php +++ b/app/Services/MagentoProductMigrationService.php @@ -2744,13 +2744,40 @@ protected function migrateCatalogProductOptions() $m2ProductId = $productIdMapping[$m1ProductId]; - // Check if option already exists in M2 (by product_id and type) - $existingOption = DB::connection($this->magento2Connection) + // Check if option already exists in M2 + // Match by product_id, type, sort_order, and sku (if sku is not null/empty) + // This ensures we find the exact same option and update it instead of creating duplicates + $m1Sku = $m1Option->sku ?? null; + $m1SortOrder = $m1Option->sort_order ?? 0; + + $query = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'catalog_product_option') ->where('product_id', $m2ProductId) ->where('type', $m1Option->type) - ->where('sku', $m1Option->sku ?? '') - ->first(); + ->where('sort_order', $m1SortOrder); + + // If SKU is provided and not empty, include it in the match + if (!empty($m1Sku)) { + $query->where('sku', $m1Sku); + } else { + // If SKU is null/empty, match options where SKU is also null/empty + $query->where(function($q) { + $q->whereNull('sku')->orWhere('sku', ''); + }); + } + + $existingOption = $query->first(); + + // If still no match, try matching by product_id, type, and sort_order only + // This catches cases where SKU might differ but it's the same option + if (!$existingOption) { + $existingOption = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option') + ->where('product_id', $m2ProductId) + ->where('type', $m1Option->type) + ->where('sort_order', $m1SortOrder) + ->first(); + } $optionData = [ 'product_id' => $m2ProductId, diff --git a/assets/Screenshot_From_2025-11-30_22-33-37-3161ad9d-3ff4-4a65-80fc-7e67b3e01c72.png b/assets/Screenshot_From_2025-11-30_22-33-37-3161ad9d-3ff4-4a65-80fc-7e67b3e01c72.png new file mode 100644 index 0000000..e69de29 diff --git a/assets/image-691a52f0-582a-4795-918b-7378da7c637d.png b/assets/image-691a52f0-582a-4795-918b-7378da7c637d.png new file mode 100644 index 0000000..e69de29 diff --git a/resources/js/customers.js b/resources/js/customers.js index d596c71..a8beb7c 100644 --- a/resources/js/customers.js +++ b/resources/js/customers.js @@ -9,39 +9,115 @@ document.addEventListener('DOMContentLoaded', function() { routes = window.customerRoutes; csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; } + + // Attach event listener to the button instead of using inline onclick + const startButton = document.getElementById('startCustomerMigrationBtn'); + if (startButton) { + startButton.addEventListener('click', function(e) { + e.preventDefault(); + startCustomerMigration(); + }); + } }); -function startCustomerMigration(dryRun) { - const button = dryRun ? document.getElementById('dryRunCustomerMigrationBtn') : document.getElementById('startCustomerMigrationBtn'); - const otherButton = dryRun ? document.getElementById('startCustomerMigrationBtn') : document.getElementById('dryRunCustomerMigrationBtn'); - const originalText = button.textContent; - button.disabled = true; - otherButton.disabled = true; - button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...'; - button.style.cursor = 'not-allowed'; +function startCustomerMigration() { + try { + const button = document.getElementById('startCustomerMigrationBtn'); + if (!button) { + console.error('Start customer migration button not found'); + alert('Error: Button not found. Please refresh the page.'); + return; + } + + const logContent = document.getElementById('customerMigrationLogContent'); + if (!logContent) { + console.error('Customer migration log content not found'); + alert('Error: Log container not found. Please refresh the page.'); + return; + } + + const originalText = button.textContent || 'Start Customer Migration'; + if (button) { + button.disabled = true; + button.textContent = 'Migrating...'; + button.style.cursor = 'not-allowed'; + } - const logContent = document.getElementById('customerMigrationLogContent'); - logContent.innerHTML = '
' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '
'; + const progressContainer = document.getElementById('migrationProgressContainer'); + + // Show progress bar + if (progressContainer) { + progressContainer.style.display = 'block'; + updateProgressBar(0, 0, 0, 0, 0, 'Starting migration...', ''); + } + + logContent.innerHTML = '
Starting migration...
'; + + // Generate progress key for tracking + const progressKey = 'customer_migration_progress_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + let progressInterval = null; + + // Check if routes are available + if (!routes || !routes.migrateCustomers) { + console.error('Customer routes not available', routes); + if (button) { + button.disabled = false; + button.textContent = originalText; + button.style.cursor = 'pointer'; + } + alert('Error: Routes not initialized. Please refresh the page.'); + return; + } + + progressInterval = pollMigrationProgress(progressKey, button, originalText, logContent); fetch(routes.migrateCustomers, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfToken + 'X-CSRF-TOKEN': csrfToken, + 'Accept': 'application/json' }, - body: JSON.stringify({ dry_run: dryRun }) + body: JSON.stringify({ dry_run: false, progress_key: progressKey }) + }) + .then(async response => { + const contentType = response.headers.get('content-type'); + + // Check if response is JSON + if (contentType && contentType.includes('application/json')) { + return response.json(); + } else { + // Response is HTML (error page), get text and show error + const text = await response.text(); + throw new Error(`Server returned HTML instead of JSON. Status: ${response.status}. This usually means there was a server error. Check the browser console or server logs for details.`); + } }) - .then(response => response.json()) .then(data => { - button.disabled = false; - otherButton.disabled = false; - button.textContent = originalText; - button.style.cursor = 'pointer'; + // Stop polling + if (progressInterval) { + clearInterval(progressInterval); + } + + // Hide progress bar + if (progressContainer) { + progressContainer.style.display = 'none'; + } + + if (button) { + button.disabled = false; + button.textContent = originalText; + button.style.cursor = 'pointer'; + } if (data.success) { + // Update progress bar to 100% before hiding + if (progressContainer) { + updateProgressBar(100, data.added + data.updated, data.added + data.updated, data.added, data.updated, data.errors, 'completed', ''); + } + const successEntry = document.createElement('div'); successEntry.className = 'log-entry success'; - successEntry.textContent = `✓ ${dryRun ? 'Dry run' : 'Migration'} completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`; + successEntry.textContent = `✓ Migration completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`; logContent.appendChild(successEntry); if (data.log && data.log.length > 0) { @@ -58,21 +134,116 @@ function startCustomerMigration(dryRun) { } else { const errorEntry = document.createElement('div'); errorEntry.className = 'log-entry error'; - errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error'); + errorEntry.textContent = '✗ Migration failed: ' + (data.message || 'Unknown error'); logContent.appendChild(errorEntry); } }) .catch(error => { - button.disabled = false; - otherButton.disabled = false; - button.textContent = originalText; - button.style.cursor = 'pointer'; + // Stop polling + if (progressInterval) { + clearInterval(progressInterval); + } + + // Hide progress bar + if (progressContainer) { + progressContainer.style.display = 'none'; + } + + if (button) { + button.disabled = false; + button.textContent = originalText; + button.style.cursor = 'pointer'; + } - const errorEntry = document.createElement('div'); - errorEntry.className = 'log-entry error'; - errorEntry.textContent = '✗ Error: ' + error.message; - logContent.appendChild(errorEntry); + if (logContent) { + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ Error: ' + error.message; + logContent.appendChild(errorEntry); + } else { + console.error('Error during migration:', error); + alert('Error: ' + error.message); + } }); + } catch (error) { + console.error('Error in startCustomerMigration:', error); + alert('An unexpected error occurred. Please refresh the page and try again.'); + } +} + +function pollMigrationProgress(progressKey, button, originalText, logContent) { + return setInterval(() => { + if (!routes.migrationProgress) { + console.error('Migration progress route not available'); + return; + } + + fetch(routes.migrationProgress + '?progress_key=' + encodeURIComponent(progressKey), { + method: 'GET', + headers: { + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success && data.progress) { + const progress = data.progress; + const percentage = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0; + + updateProgressBar( + percentage, + progress.current, + progress.total, + progress.added, + progress.updated, + progress.errors, + progress.status, + progress.current_email + ); + } else if (data.success === false && data.message === 'Progress not found') { + // Progress not found yet, migration might not have started + // This is okay, just continue polling + } + }) + .catch(error => { + console.error('Error polling progress:', error); + }); + }, 500); // Poll every 500ms for smoother updates +} + +function updateProgressBar(percentage, current, total, added, updated, errors, status, currentEmail) { + const progressBar = document.getElementById('progressBar'); + const progressPercentage = document.getElementById('progressPercentage'); + const progressStatus = document.getElementById('progressStatus'); + const currentCustomer = document.getElementById('currentCustomer'); + const progressAdded = document.getElementById('progressAdded'); + const progressUpdated = document.getElementById('progressUpdated'); + const progressErrors = document.getElementById('progressErrors'); + + if (progressBar) { + progressBar.style.width = percentage + '%'; + } + if (progressPercentage) { + progressPercentage.textContent = percentage + '%'; + } + if (progressStatus) { + const statusText = status === 'running' ? 'Migrating...' : + status === 'completed' ? 'Migration completed!' : + status === 'failed' ? 'Migration failed!' : 'Starting...'; + progressStatus.textContent = statusText; + } + if (currentCustomer) { + currentCustomer.textContent = currentEmail ? `Current: ${currentEmail}` : `Processing ${current} of ${total}`; + } + if (progressAdded) { + progressAdded.textContent = added; + } + if (progressUpdated) { + progressUpdated.textContent = updated; + } + if (progressErrors) { + progressErrors.textContent = errors; + } } function deleteM2Customer(customerId, email, firstname, lastname) { diff --git a/resources/views/customers/index.blade.php b/resources/views/customers/index.blade.php index f9a9a6a..e0648d5 100644 --- a/resources/views/customers/index.blade.php +++ b/resources/views/customers/index.blade.php @@ -18,13 +18,27 @@
- -
+ + + @@ -34,7 +48,7 @@
- No customer migration logs yet. Click "Run Dry Run" or "Start Customer Migration" to begin. + No customer migration logs yet. Click "Start Customer Migration" to begin.
@@ -154,7 +168,8 @@ class="btn btn-danger" @endpush diff --git a/routes/web.php b/routes/web.php index 0286d8b..e40bec4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -63,5 +63,6 @@ Route::prefix('customers')->name('customers.')->group(function () { Route::get('/', [CustomersController::class, 'index'])->name('index'); Route::post('/migrate', [CustomersController::class, 'migrateCustomers'])->name('migrate-customers'); + Route::get('/migration-progress', [CustomersController::class, 'getMigrationProgress'])->name('migration-progress'); Route::delete('/{customerId}', [CustomersController::class, 'deleteM2Customer'])->name('delete-customer'); });