From fecc231e30a3770912c6dfeac369231cdac5c9b7 Mon Sep 17 00:00:00 2001 From: Chris Rosenau Date: Thu, 11 Dec 2025 23:19:05 -0700 Subject: [PATCH] Added order syncing --- app/Http/Controllers/CustomersController.php | 31 ++ app/Http/Controllers/OrdersController.php | 169 +++++++++ .../MagentoCategoryMigrationService.php | 340 ++++++++++++++++++ resources/js/customers.js | 37 ++ resources/js/orders.js | 318 ++++++++++++++++ resources/views/customers/index.blade.php | 11 +- resources/views/orders/index.blade.php | 178 +++++++++ resources/views/partials/navigation.blade.php | 3 + routes/web.php | 11 + vite.config.js | 1 + 10 files changed, 1094 insertions(+), 5 deletions(-) create mode 100644 app/Http/Controllers/OrdersController.php create mode 100644 resources/js/orders.js create mode 100644 resources/views/orders/index.blade.php diff --git a/app/Http/Controllers/CustomersController.php b/app/Http/Controllers/CustomersController.php index b95bec3..4324a82 100644 --- a/app/Http/Controllers/CustomersController.php +++ b/app/Http/Controllers/CustomersController.php @@ -116,6 +116,37 @@ public function getMigrationProgress(Request $request) } } + /** + * Get customer statistics + */ + public function getStatistics() + { + try { + $m1Customers = $this->migrationService->getMagento1Customers(); + $m2Customers = $this->migrationService->getMagento2Customers(); + $m1CustomersNotInM2 = $this->migrationService->getM1CustomersNotInM2(); + $m2CustomersNotInM1 = $this->migrationService->getM2CustomersNotInM1(); + + return response()->json([ + 'success' => true, + 'statistics' => [ + 'm1Customers' => $m1Customers->count(), + 'm2Customers' => $m2Customers->count(), + 'm1CustomersNotInM2' => $m1CustomersNotInM2->count(), + 'm2CustomersNotInM1' => $m2CustomersNotInM1->count(), + ] + ]); + + } catch (\Exception $e) { + Log::error('Get customer statistics error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to get statistics: ' . $e->getMessage() + ], 500); + } + } + /** * Delete a single customer from Magento 2 */ diff --git a/app/Http/Controllers/OrdersController.php b/app/Http/Controllers/OrdersController.php new file mode 100644 index 0000000..a11c642 --- /dev/null +++ b/app/Http/Controllers/OrdersController.php @@ -0,0 +1,169 @@ +migrationService = $migrationService; + } + + /** + * Show the orders page + */ + public function index() + { + $m1Orders = $this->migrationService->getMagento1Orders(); + $m2Orders = $this->migrationService->getMagento2Orders(); + $m1OrdersNotInM2 = $this->migrationService->getM1OrdersNotInM2(); + $m2OrdersNotInM1 = $this->migrationService->getM2OrdersNotInM1(); + + return view('orders.index', [ + 'm1Orders' => $m1Orders, + 'm2Orders' => $m2Orders, + 'm1OrdersNotInM2' => $m1OrdersNotInM2, + 'm2OrdersNotInM1' => $m2OrdersNotInM1, + ]); + } + + /** + * Migrate all orders from Magento 1 to Magento 2 + */ + public function migrateOrders(Request $request) + { + try { + // 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->migrateOrders($dryRun, $progressKey); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Order migration error: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [] + ], 500); + } + } + + /** + * Get order 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 order migration progress error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to get progress: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get order statistics + */ + public function getStatistics() + { + try { + $m1Orders = $this->migrationService->getMagento1Orders(); + $m2Orders = $this->migrationService->getMagento2Orders(); + $m1OrdersNotInM2 = $this->migrationService->getM1OrdersNotInM2(); + $m2OrdersNotInM1 = $this->migrationService->getM2OrdersNotInM1(); + + return response()->json([ + 'success' => true, + 'statistics' => [ + 'm1Orders' => $m1Orders->count(), + 'm2Orders' => $m2Orders->count(), + 'm1OrdersNotInM2' => $m1OrdersNotInM2->count(), + 'm2OrdersNotInM1' => $m2OrdersNotInM1->count(), + ] + ]); + + } catch (\Exception $e) { + Log::error('Get order statistics error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to get statistics: ' . $e->getMessage() + ], 500); + } + } + + /** + * Delete a single order from Magento 2 + */ + public function deleteM2Order(Request $request, $orderId) + { + try { + $result = $this->migrationService->deleteM2Order($orderId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Delete M2 order error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index dc41551..1d806ac 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -6581,5 +6581,345 @@ protected function updateM2Record($m2Table, $rowArray) ->update($cleanData); } } + + /** + * Get all orders from Magento 1 + */ + public function getMagento1Orders() + { + try { + $orders = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'sales_flat_order') + ->select('entity_id', 'increment_id', 'customer_email', 'status', 'grand_total', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + + return $orders; + } catch (Exception $e) { + Log::error('Error fetching Magento 1 orders: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Get all orders from Magento 2 + */ + public function getMagento2Orders() + { + try { + $orders = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'sales_order') + ->select('entity_id', 'increment_id', 'customer_email', 'status', 'grand_total', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + + return $orders; + } catch (Exception $e) { + Log::error('Error fetching Magento 2 orders: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Get Magento 1 orders that don't exist in Magento 2 + */ + public function getM1OrdersNotInM2() + { + try { + $m1Orders = $this->getMagento1Orders(); + $m2Orders = $this->getMagento2Orders(); + + // Get all M2 increment IDs - create a set for faster lookup + $m2IncrementIdsSet = []; + foreach ($m2Orders as $m2Order) { + $incrementId = $m2Order->increment_id ?? null; + if (!empty($incrementId) && is_string($incrementId)) { + $normalizedIncrementId = trim($incrementId); + if (!empty($normalizedIncrementId)) { + $m2IncrementIdsSet[$normalizedIncrementId] = true; + } + } + } + + // Filter M1 orders that don't exist in M2 + $missingOrders = collect(); + foreach ($m1Orders as $m1Order) { + $incrementId = $m1Order->increment_id ?? null; + if (!empty($incrementId) && is_string($incrementId)) { + $normalizedIncrementId = trim($incrementId); + if (!empty($normalizedIncrementId) && !isset($m2IncrementIdsSet[$normalizedIncrementId])) { + $missingOrders->push($m1Order); + } + } + } + + return $missingOrders->values(); + } catch (Exception $e) { + Log::error('Error fetching missing orders: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Get Magento 2 orders that don't exist in Magento 1 + */ + public function getM2OrdersNotInM1() + { + try { + $m1Orders = $this->getMagento1Orders(); + $m2Orders = $this->getMagento2Orders(); + + // Get all M1 increment IDs - create a set for faster lookup + $m1IncrementIdsSet = []; + foreach ($m1Orders as $m1Order) { + $incrementId = $m1Order->increment_id ?? null; + if (!empty($incrementId) && is_string($incrementId)) { + $normalizedIncrementId = trim($incrementId); + if (!empty($normalizedIncrementId)) { + $m1IncrementIdsSet[$normalizedIncrementId] = true; + } + } + } + + // Filter M2 orders that don't exist in M1 + $missingOrders = collect(); + foreach ($m2Orders as $m2Order) { + $incrementId = $m2Order->increment_id ?? null; + if (!empty($incrementId) && is_string($incrementId)) { + $normalizedIncrementId = trim($incrementId); + if (!empty($normalizedIncrementId) && !isset($m1IncrementIdsSet[$normalizedIncrementId])) { + $missingOrders->push($m2Order); + } + } + } + + return $missingOrders->values(); + } catch (Exception $e) { + Log::error('Error fetching M2 orders not in M1: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Migrate all orders from Magento 1 to Magento 2 + */ + public function migrateOrders($dryRun = false, $progressKey = null) + { + try { + $this->migrationLog = []; + $addedCount = 0; + $updatedCount = 0; + $errorCount = 0; + + // Get all M1 orders + $m1Orders = $this->getMagento1Orders(); + $totalOrders = $m1Orders->count(); + + // Initialize progress tracking + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => $totalOrders, + 'current' => 0, + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'status' => 'running', + 'current_increment_id' => '' + ], 3600); + } + + if (!$dryRun) { + DB::connection($this->magento2Connection)->beginTransaction(); + } + + $currentIndex = 0; + foreach ($m1Orders as $m1Order) { + $currentIndex++; + try { + $m1IncrementId = !empty($m1Order->increment_id) ? trim($m1Order->increment_id) : null; + + // Update progress if tracking enabled + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => $totalOrders, + 'current' => $currentIndex, + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'status' => 'running', + 'current_increment_id' => $m1IncrementId ?? 'N/A' + ], 3600); + } + + if (empty($m1IncrementId)) { + $this->migrationLog[] = "SKIPPED: Order ID {$m1Order->entity_id} - no increment_id"; + continue; + } + + // Check if order exists in M2 by increment_id + $m2Order = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'sales_order') + ->where('increment_id', $m1IncrementId) + ->first(); + + $m2OrderId = null; + $isNew = false; + + if ($m2Order) { + // Order exists, update + $m2OrderId = $m2Order->entity_id; + if (!$dryRun) { + // Update order data + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'sales_order') + ->where('entity_id', $m2OrderId) + ->update([ + 'customer_email' => $m1Order->customer_email ?? null, + 'status' => $m1Order->status ?? null, + 'grand_total' => $m1Order->grand_total ?? 0, + 'updated_at' => $m1Order->updated_at ?? now(), + ]); + $this->migrationLog[] = "Updating existing order: {$m1IncrementId} (ID: {$m2OrderId})"; + } else { + $this->migrationLog[] = "Would update existing order: {$m1IncrementId} (ID: {$m2OrderId})"; + } + $updatedCount++; + } else { + // Order doesn't exist, create + if (!$dryRun) { + // Insert order entity + $m2OrderId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'sales_order') + ->insertGetId([ + 'increment_id' => $m1IncrementId, + 'customer_email' => $m1Order->customer_email ?? null, + 'status' => $m1Order->status ?? 'pending', + 'grand_total' => $m1Order->grand_total ?? 0, + 'created_at' => $m1Order->created_at ?? now(), + 'updated_at' => $m1Order->updated_at ?? now(), + ]); + $this->migrationLog[] = "Added new order: {$m1IncrementId} (ID: {$m2OrderId})"; + } else { + $this->migrationLog[] = "Would add new order: {$m1IncrementId}"; + $m2OrderId = 0; // Placeholder for dry run + } + $addedCount++; + $isNew = true; + } + + // Note: Full order migration would also migrate: + // - Order items (sales_order_item) + // - Order addresses (sales_order_address) + // - Order payment (sales_order_payment) + // - Order status history (sales_order_status_history) + // This is a simplified version that only migrates the main order record + + } catch (Exception $e) { + $errorCount++; + $m1IncrementId = $m1Order->increment_id ?? 'N/A'; + $this->migrationLog[] = "ERROR: Failed to migrate order {$m1IncrementId}: " . $e->getMessage(); + Log::error("Error migrating order {$m1IncrementId}: " . $e->getMessage()); + } + } + + if (!$dryRun) { + DB::connection($this->magento2Connection)->commit(); + } + + // Update progress to completed + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => $totalOrders, + 'current' => $totalOrders, + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'status' => 'completed', + 'current_increment_id' => '' + ], 3600); + } + + return [ + 'success' => true, + 'message' => $dryRun ? 'Dry run completed' : 'Order migration completed', + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog + ]; + + } catch (Exception $e) { + if (!$dryRun) { + DB::connection($this->magento2Connection)->rollBack(); + } + + // Update progress to failed + if ($progressKey && !$dryRun) { + Cache::put($progressKey, [ + 'total' => isset($totalOrders) ? $totalOrders : 0, + 'current' => isset($currentIndex) ? $currentIndex : 0, + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'status' => 'failed', + 'current_increment_id' => '' + ], 3600); + } + + Log::error('Error migrating orders: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + } + + /** + * Delete a single order from Magento 2 + */ + public function deleteM2Order($orderId) + { + try { + $order = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'sales_order') + ->where('entity_id', $orderId) + ->first(); + + if (!$order) { + return [ + 'success' => false, + 'message' => 'Order not found' + ]; + } + + $incrementId = $order->increment_id ?? 'N/A'; + + // Delete order (cascade deletes should handle related records) + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'sales_order') + ->where('entity_id', $orderId) + ->delete(); + + return [ + 'success' => true, + 'message' => "Order {$incrementId} deleted successfully" + ]; + + } catch (Exception $e) { + Log::error('Error deleting M2 order: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage() + ]; + } + } } diff --git a/resources/js/customers.js b/resources/js/customers.js index a8beb7c..26a864a 100644 --- a/resources/js/customers.js +++ b/resources/js/customers.js @@ -115,6 +115,9 @@ function startCustomerMigration() { updateProgressBar(100, data.added + data.updated, data.added + data.updated, data.added, data.updated, data.errors, 'completed', ''); } + // Update customer statistics + updateCustomerStatistics(); + const successEntry = document.createElement('div'); successEntry.className = 'log-entry success'; successEntry.textContent = `✓ Migration completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`; @@ -211,6 +214,40 @@ function pollMigrationProgress(progressKey, button, originalText, logContent) { }, 500); // Poll every 500ms for smoother updates } +function updateCustomerStatistics() { + if (!routes.statistics) { + console.error('Statistics route not available'); + return; + } + + fetch(routes.statistics, { + method: 'GET', + headers: { + 'X-CSRF-TOKEN': csrfToken, + 'Accept': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success && data.statistics) { + const stats = data.statistics; + + const m1CustomersEl = document.getElementById('statM1Customers'); + const m2CustomersEl = document.getElementById('statM2Customers'); + const m1NotInM2El = document.getElementById('statM1NotInM2'); + const m2NotInM1El = document.getElementById('statM2NotInM1'); + + if (m1CustomersEl) m1CustomersEl.textContent = stats.m1Customers; + if (m2CustomersEl) m2CustomersEl.textContent = stats.m2Customers; + if (m1NotInM2El) m1NotInM2El.textContent = stats.m1CustomersNotInM2; + if (m2NotInM1El) m2NotInM1El.textContent = stats.m2CustomersNotInM1; + } + }) + .catch(error => { + console.error('Error updating customer statistics:', error); + }); +} + function updateProgressBar(percentage, current, total, added, updated, errors, status, currentEmail) { const progressBar = document.getElementById('progressBar'); const progressPercentage = document.getElementById('progressPercentage'); diff --git a/resources/js/orders.js b/resources/js/orders.js new file mode 100644 index 0000000..80297b7 --- /dev/null +++ b/resources/js/orders.js @@ -0,0 +1,318 @@ +// Orders page JavaScript + +let routes = {}; +let csrfToken = ''; + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + if (window.orderRoutes) { + routes = window.orderRoutes; + csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; + } + + // Attach event listener to the button instead of using inline onclick + const startButton = document.getElementById('startOrderMigrationBtn'); + if (startButton) { + startButton.addEventListener('click', function(e) { + e.preventDefault(); + startOrderMigration(); + }); + } +}); + +function startOrderMigration() { + try { + const button = document.getElementById('startOrderMigrationBtn'); + if (!button) { + console.error('Start order migration button not found'); + alert('Error: Button not found. Please refresh the page.'); + return; + } + + const logContent = document.getElementById('orderMigrationLogContent'); + if (!logContent) { + console.error('Order migration log content not found'); + alert('Error: Log container not found. Please refresh the page.'); + return; + } + + const originalText = button.textContent || 'Start Order Migration'; + if (button) { + button.disabled = true; + button.textContent = 'Migrating...'; + button.style.cursor = 'not-allowed'; + } + + 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 = 'order_migration_progress_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + let progressInterval = null; + + // Check if routes are available + if (!routes || !routes.migrateOrders) { + console.error('Order 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.migrateOrders, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + 'Accept': 'application/json' + }, + 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(data => { + // 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', ''); + } + + // Update order statistics + updateOrderStatistics(); + + const successEntry = document.createElement('div'); + successEntry.className = 'log-entry success'; + 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) { + data.log.forEach(log => { + const entry = document.createElement('div'); + entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : 'success'); + entry.textContent = log; + logContent.appendChild(entry); + }); + } + + const logContainer = document.getElementById('orderMigrationLogContainer'); + logContainer.scrollTop = logContainer.scrollHeight; + } else { + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ Migration failed: ' + (data.message || 'Unknown error'); + logContent.appendChild(errorEntry); + } + }) + .catch(error => { + // 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 (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 startOrderMigration:', 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_increment_id + ); + } 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, currentIncrementId) { + const progressBar = document.getElementById('progressBar'); + const progressPercentage = document.getElementById('progressPercentage'); + const progressStatus = document.getElementById('progressStatus'); + const currentOrder = document.getElementById('currentOrder'); + 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 (currentOrder) { + currentOrder.textContent = currentIncrementId ? `Current: ${currentIncrementId}` : `Processing ${current} of ${total}`; + } + if (progressAdded) { + progressAdded.textContent = added; + } + if (progressUpdated) { + progressUpdated.textContent = updated; + } + if (progressErrors) { + progressErrors.textContent = errors; + } +} + +function updateOrderStatistics() { + if (!routes.statistics) { + console.error('Statistics route not available'); + return; + } + + fetch(routes.statistics, { + method: 'GET', + headers: { + 'X-CSRF-TOKEN': csrfToken, + 'Accept': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success && data.statistics) { + const stats = data.statistics; + + const m1OrdersEl = document.getElementById('statM1Orders'); + const m2OrdersEl = document.getElementById('statM2Orders'); + const m1NotInM2El = document.getElementById('statM1NotInM2'); + const m2NotInM1El = document.getElementById('statM2NotInM1'); + + if (m1OrdersEl) m1OrdersEl.textContent = stats.m1Orders; + if (m2OrdersEl) m2OrdersEl.textContent = stats.m2Orders; + if (m1NotInM2El) m1NotInM2El.textContent = stats.m1OrdersNotInM2; + if (m2NotInM1El) m2NotInM1El.textContent = stats.m2OrdersNotInM1; + } + }) + .catch(error => { + console.error('Error updating order statistics:', error); + }); +} + +function deleteM2Order(orderId, incrementId) { + const orderLabel = incrementId !== 'N/A' ? incrementId : `ID ${orderId}`; + + if (!confirm(`Are you sure you want to delete order "${orderLabel}"?`)) { + return; + } + + const url = routes.deleteOrder.replace(':id', orderId); + + fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Order deleted successfully!'); + location.reload(); + } else { + alert('Error: ' + (data.message || 'Failed to delete order')); + } + }) + .catch(error => { + alert('Error: ' + error.message); + }); +} + +// Make functions available globally +window.startOrderMigration = startOrderMigration; +window.deleteM2Order = deleteM2Order; diff --git a/resources/views/customers/index.blade.php b/resources/views/customers/index.blade.php index e0648d5..bc9ff2b 100644 --- a/resources/views/customers/index.blade.php +++ b/resources/views/customers/index.blade.php @@ -59,19 +59,19 @@

📊 Customer Statistics

-
{{ $m1Customers->count() }}
+
{{ $m1Customers->count() }}
Magento 1 Customers
-
{{ $m2Customers->count() }}
+
{{ $m2Customers->count() }}
Magento 2 Customers
-
{{ $m1CustomersNotInM2->count() }}
+
{{ $m1CustomersNotInM2->count() }}
M1 Customers Not in M2
-
{{ $m2CustomersNotInM1->count() }}
+
{{ $m2CustomersNotInM1->count() }}
M2 Customers Not in M1
@@ -169,7 +169,8 @@ class="btn btn-danger" window.customerRoutes = { migrateCustomers: '{{ route("customers.migrate-customers") }}', deleteCustomer: '{{ route("customers.delete-customer", ["customerId" => ":id"]) }}', - migrationProgress: '{{ route("customers.migration-progress") }}' + migrationProgress: '{{ route("customers.migration-progress") }}', + statistics: '{{ route("customers.statistics") }}' }; @endpush diff --git a/resources/views/orders/index.blade.php b/resources/views/orders/index.blade.php new file mode 100644 index 0000000..d6a3362 --- /dev/null +++ b/resources/views/orders/index.blade.php @@ -0,0 +1,178 @@ +@extends('layouts.app') + +@section('content') + +
+

🛒 Sales Order Migration

+
+

What happens when you click "Start Order Migration"?

+

The order migration process will:

+
    +
  • Create new orders: If an order with the same increment_id doesn't exist in Magento 2, it will be created with all its data
  • +
  • Update existing orders: If an order with the same increment_id already exists in Magento 2, it will be updated with the latest data from Magento 1
  • +
  • Migrate order items: All order items including products, quantities, prices, and options will be migrated
  • +
  • Migrate order addresses: Billing and shipping addresses will be migrated
  • +
  • Migrate order payments: Payment information and transaction data will be migrated
  • +
  • Preserve order data: Order status, timestamps, and customer associations will be preserved
  • +
  • Generate logs: Detailed migration logs showing which orders were added, updated, or encountered errors
  • +
+

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

+
+ +
+ +
+ + + +
+ + +
+

📋 Order Migration Logs

+

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

+
+
+
+ No order migration logs yet. Click "Start Order Migration" to begin. +
+
+
+
+ + +
+

📊 Order Statistics

+
+
+
{{ $m1Orders->count() }}
+
Magento 1 Orders
+
+
+
{{ $m2Orders->count() }}
+
Magento 2 Orders
+
+
+
{{ $m1OrdersNotInM2->count() }}
+
M1 Orders Not in M2
+
+
+
{{ $m2OrdersNotInM1->count() }}
+
M2 Orders Not in M1
+
+
+
+ + + @if($m1OrdersNotInM2->count() > 0) +
+

⚠️ Magento 1 Orders Not in Magento 2

+

These orders exist in Magento 1 but are missing in Magento 2:

+
+ + + + + + + + + + + + + @foreach($m1OrdersNotInM2->take(50) as $order) + + + + + + + + + @endforeach + +
IDIncrement IDCustomer EmailStatusGrand TotalCreated At
{{ $order->entity_id }}{{ $order->increment_id ?? 'N/A' }}{{ $order->customer_email ?? 'N/A' }}{{ $order->status ?? 'N/A' }}{{ $order->grand_total ?? 'N/A' }}{{ $order->created_at ?? 'N/A' }}
+ @if($m1OrdersNotInM2->count() > 50) +

Showing first 50 of {{ $m1OrdersNotInM2->count() }} orders.

+ @endif +
+
+ @endif + + + @if($m2OrdersNotInM1->count() > 0) +
+

⚠️ Magento 2 Orders Not in Magento 1

+

These orders exist in Magento 2 but are missing in Magento 1:

+
+ + + + + + + + + + + + + + @foreach($m2OrdersNotInM1->take(50) as $order) + + + + + + + + + + @endforeach + +
IDIncrement IDCustomer EmailStatusGrand TotalCreated AtActions
{{ $order->entity_id }}{{ $order->increment_id ?? 'N/A' }}{{ $order->customer_email ?? 'N/A' }}{{ $order->status ?? 'N/A' }}{{ $order->grand_total ?? 'N/A' }}{{ $order->created_at ?? 'N/A' }} + +
+ @if($m2OrdersNotInM1->count() > 50) +

Showing first 50 of {{ $m2OrdersNotInM1->count() }} orders.

+ @endif +
+
+ @endif +@endsection + +{{-- CSS is loaded globally via app.css --}} + +@push('scripts') + @vite(['resources/js/orders.js']) + +@endpush diff --git a/resources/views/partials/navigation.blade.php b/resources/views/partials/navigation.blade.php index b0a16ed..282107c 100644 --- a/resources/views/partials/navigation.blade.php +++ b/resources/views/partials/navigation.blade.php @@ -17,5 +17,8 @@ Customers + + Orders + diff --git a/routes/web.php b/routes/web.php index e40bec4..005a7ce 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\AttributesController; use App\Http\Controllers\ProductsController; use App\Http\Controllers\CustomersController; +use App\Http\Controllers\OrdersController; Route::get('/', function () { return redirect('/connections'); @@ -62,7 +63,17 @@ // Customers routes Route::prefix('customers')->name('customers.')->group(function () { Route::get('/', [CustomersController::class, 'index'])->name('index'); + Route::get('/statistics', [CustomersController::class, 'getStatistics'])->name('statistics'); 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'); }); + +// Orders routes +Route::prefix('orders')->name('orders.')->group(function () { + Route::get('/', [OrdersController::class, 'index'])->name('index'); + Route::get('/statistics', [OrdersController::class, 'getStatistics'])->name('statistics'); + Route::post('/migrate', [OrdersController::class, 'migrateOrders'])->name('migrate-orders'); + Route::get('/migration-progress', [OrdersController::class, 'getMigrationProgress'])->name('migration-progress'); + Route::delete('/{orderId}', [OrdersController::class, 'deleteM2Order'])->name('delete-order'); +}); diff --git a/vite.config.js b/vite.config.js index 22e06cf..e1d61f9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,6 +14,7 @@ export default defineConfig({ 'resources/js/attributes.js', 'resources/js/products.js', 'resources/js/customers.js', + 'resources/js/orders.js', ], refresh: true, }),