Added order syncing

This commit is contained in:
Chris Rosenau 2025-12-11 23:19:05 -07:00
parent 353f9e0564
commit fecc231e30
10 changed files with 1094 additions and 5 deletions

View File

@ -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
*/

View File

@ -0,0 +1,169 @@
<?php
namespace App\Http\Controllers;
use App\Services\MagentoCategoryMigrationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class OrdersController extends Controller
{
protected $migrationService;
public function __construct(MagentoCategoryMigrationService $migrationService)
{
$this->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);
}
}
}

View File

@ -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()
];
}
}
}

View File

@ -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');

318
resources/js/orders.js Normal file
View File

@ -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 = '<div class="log-entry">Starting migration...</div>';
// 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;

View File

@ -59,19 +59,19 @@
<h2>📊 Customer Statistics</h2>
<div class="stats">
<div class="stat-card">
<div class="number">{{ $m1Customers->count() }}</div>
<div class="number" id="statM1Customers">{{ $m1Customers->count() }}</div>
<div class="label">Magento 1 Customers</div>
</div>
<div class="stat-card">
<div class="number">{{ $m2Customers->count() }}</div>
<div class="number" id="statM2Customers">{{ $m2Customers->count() }}</div>
<div class="label">Magento 2 Customers</div>
</div>
<div class="stat-card">
<div class="number">{{ $m1CustomersNotInM2->count() }}</div>
<div class="number" id="statM1NotInM2">{{ $m1CustomersNotInM2->count() }}</div>
<div class="label">M1 Customers Not in M2</div>
</div>
<div class="stat-card">
<div class="number">{{ $m2CustomersNotInM1->count() }}</div>
<div class="number" id="statM2NotInM1">{{ $m2CustomersNotInM1->count() }}</div>
<div class="label">M2 Customers Not in M1</div>
</div>
</div>
@ -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") }}'
};
</script>
@endpush

View File

@ -0,0 +1,178 @@
@extends('layouts.app')
@section('content')
<!-- Order Migration Section -->
<div class="section">
<h2>🛒 Sales Order Migration</h2>
<div class="info-box" style="margin-bottom: 20px;">
<h3 style="margin-bottom: 10px; color: #1976D2; font-size: 1.1em;">What happens when you click "Start Order Migration"?</h3>
<p style="margin: 5px 0; color: #1976D2;">The order migration process will:</p>
<ul style="margin: 10px 0 0 20px; color: #1976D2; line-height: 1.8;">
<li><strong>Create new orders:</strong> If an order with the same increment_id doesn't exist in Magento 2, it will be created with all its data</li>
<li><strong>Update existing orders:</strong> If an order with the same increment_id already exists in Magento 2, it will be updated with the latest data from Magento 1</li>
<li><strong>Migrate order items:</strong> All order items including products, quantities, prices, and options will be migrated</li>
<li><strong>Migrate order addresses:</strong> Billing and shipping addresses will be migrated</li>
<li><strong>Migrate order payments:</strong> Payment information and transaction data will be migrated</li>
<li><strong>Preserve order data:</strong> Order status, timestamps, and customer associations will be preserved</li>
<li><strong>Generate logs:</strong> Detailed migration logs showing which orders were added, updated, or encountered errors</li>
</ul>
<p style="margin: 10px 0 0 0; color: #d32f2f; font-weight: 600;">⚠️ <strong>Warning:</strong> This will modify your Magento 2 database. Make sure you have a backup before proceeding.</p>
</div>
<div style="margin-top: 20px; display: flex; gap: 15px; flex-wrap: wrap;">
<button id="startOrderMigrationBtn" class="btn btn-primary">
Start Order Migration
</button>
</div>
<!-- Progress Bar -->
<div id="migrationProgressContainer" style="display: none; margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span id="progressStatus" style="font-weight: 600; color: #333;">Starting migration...</span>
<span id="progressPercentage" style="font-weight: 600; color: #1976D2;">0%</span>
</div>
<div style="width: 100%; height: 24px; background-color: #e0e0e0; border-radius: 12px; overflow: hidden;">
<div id="progressBar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #1976D2 0%, #42a5f5 100%); transition: width 0.3s ease; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.85em; font-weight: 600;"></div>
</div>
<div id="progressDetails" style="margin-top: 8px; font-size: 0.9em; color: #666;">
<span id="currentOrder">-</span> |
Added: <span id="progressAdded">0</span> |
Updated: <span id="progressUpdated">0</span> |
Errors: <span id="progressErrors">0</span>
</div>
</div>
</div>
<!-- Migration Logs Section -->
<div class="section" style="margin-top: 30px;">
<h2>📋 Order Migration Logs</h2>
<p style="margin-bottom: 15px; color: #666;">Detailed logs showing which orders were added, updated, or encountered errors during migration:</p>
<div class="log-container" id="orderMigrationLogContainer" style="display: block;">
<div id="orderMigrationLogContent">
<div class="log-entry" style="color: #999; font-style: italic;">
No order migration logs yet. Click "Start Order Migration" to begin.
</div>
</div>
</div>
</div>
<!-- Order Statistics -->
<div class="section" style="margin-top: 30px;">
<h2>📊 Order Statistics</h2>
<div class="stats">
<div class="stat-card">
<div class="number" id="statM1Orders">{{ $m1Orders->count() }}</div>
<div class="label">Magento 1 Orders</div>
</div>
<div class="stat-card">
<div class="number" id="statM2Orders">{{ $m2Orders->count() }}</div>
<div class="label">Magento 2 Orders</div>
</div>
<div class="stat-card">
<div class="number" id="statM1NotInM2">{{ $m1OrdersNotInM2->count() }}</div>
<div class="label">M1 Orders Not in M2</div>
</div>
<div class="stat-card">
<div class="number" id="statM2NotInM1">{{ $m2OrdersNotInM1->count() }}</div>
<div class="label">M2 Orders Not in M1</div>
</div>
</div>
</div>
<!-- Orders Not in M2 Section -->
@if($m1OrdersNotInM2->count() > 0)
<div class="section" style="margin-top: 30px;">
<h2>⚠️ Magento 1 Orders Not in Magento 2</h2>
<p>These orders exist in Magento 1 but are missing in Magento 2:</p>
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 400px; overflow-y: auto; margin-top: 15px;">
<table class="products-table">
<thead>
<tr>
<th>ID</th>
<th>Increment ID</th>
<th>Customer Email</th>
<th>Status</th>
<th>Grand Total</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
@foreach($m1OrdersNotInM2->take(50) as $order)
<tr>
<td>{{ $order->entity_id }}</td>
<td>{{ $order->increment_id ?? 'N/A' }}</td>
<td>{{ $order->customer_email ?? 'N/A' }}</td>
<td>{{ $order->status ?? 'N/A' }}</td>
<td>{{ $order->grand_total ?? 'N/A' }}</td>
<td>{{ $order->created_at ?? 'N/A' }}</td>
</tr>
@endforeach
</tbody>
</table>
@if($m1OrdersNotInM2->count() > 50)
<p style="margin-top: 15px; color: #666;">Showing first 50 of {{ $m1OrdersNotInM2->count() }} orders.</p>
@endif
</div>
</div>
@endif
<!-- Orders Not in M1 Section -->
@if($m2OrdersNotInM1->count() > 0)
<div class="section" style="margin-top: 30px;">
<h2>⚠️ Magento 2 Orders Not in Magento 1</h2>
<p>These orders exist in Magento 2 but are missing in Magento 1:</p>
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 400px; overflow-y: auto; margin-top: 15px;">
<table class="products-table">
<thead>
<tr>
<th>ID</th>
<th>Increment ID</th>
<th>Customer Email</th>
<th>Status</th>
<th>Grand Total</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($m2OrdersNotInM1->take(50) as $order)
<tr>
<td>{{ $order->entity_id }}</td>
<td>{{ $order->increment_id ?? 'N/A' }}</td>
<td>{{ $order->customer_email ?? 'N/A' }}</td>
<td>{{ $order->status ?? 'N/A' }}</td>
<td>{{ $order->grand_total ?? 'N/A' }}</td>
<td>{{ $order->created_at ?? 'N/A' }}</td>
<td>
<button
class="btn btn-danger"
onclick="deleteM2Order({{ $order->entity_id }}, '{{ addslashes($order->increment_id ?? 'N/A') }}')"
style="padding: 6px 12px; font-size: 0.85em;">
Delete
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
@if($m2OrdersNotInM1->count() > 50)
<p style="margin-top: 15px; color: #666;">Showing first 50 of {{ $m2OrdersNotInM1->count() }} orders.</p>
@endif
</div>
</div>
@endif
@endsection
{{-- CSS is loaded globally via app.css --}}
@push('scripts')
@vite(['resources/js/orders.js'])
<script>
window.orderRoutes = {
migrateOrders: '{{ route("orders.migrate-orders") }}',
deleteOrder: '{{ route("orders.delete-order", ["orderId" => ":id"]) }}',
migrationProgress: '{{ route("orders.migration-progress") }}',
statistics: '{{ route("orders.statistics") }}'
};
</script>
@endpush

View File

@ -17,5 +17,8 @@
<a href="{{ route('customers.index') }}" class="nav-link {{ request()->routeIs('customers.*') ? 'active' : '' }}">
Customers
</a>
<a href="{{ route('orders.index') }}" class="nav-link {{ request()->routeIs('orders.*') ? 'active' : '' }}">
Orders
</a>
</nav>

View File

@ -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');
});

View File

@ -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,
}),