updated more items

This commit is contained in:
Chris Rosenau 2025-11-19 19:07:08 -07:00
parent 6afae19013
commit 659b3705fc
10 changed files with 1569 additions and 2 deletions

View File

@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers;
use App\Services\MagentoCategoryMigrationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class CustomersController extends Controller
{
protected $migrationService;
public function __construct(MagentoCategoryMigrationService $migrationService)
{
$this->migrationService = $migrationService;
}
/**
* Show the customers page
*/
public function index()
{
$m1Customers = $this->migrationService->getMagento1Customers();
$m2Customers = $this->migrationService->getMagento2Customers();
$m1CustomersNotInM2 = $this->migrationService->getM1CustomersNotInM2();
$m2CustomersNotInM1 = $this->migrationService->getM2CustomersNotInM1();
return view('customers.index', [
'm1Customers' => $m1Customers,
'm2Customers' => $m2Customers,
'm1CustomersNotInM2' => $m1CustomersNotInM2,
'm2CustomersNotInM1' => $m2CustomersNotInM1,
]);
}
/**
* Migrate all customers from Magento 1 to Magento 2
*/
public function migrateCustomers(Request $request)
{
try {
$dryRun = $request->input('dry_run', false);
$result = $this->migrationService->migrateCustomers($dryRun);
return response()->json($result, $result['success'] ? 200 : 400);
} catch (\Exception $e) {
Log::error('Customer migration error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Migration failed: ' . $e->getMessage(),
'added' => 0,
'updated' => 0,
'errors' => 0,
'log' => []
], 500);
}
}
/**
* Delete a single customer from Magento 2
*/
public function deleteM2Customer(Request $request, $customerId)
{
try {
$result = $this->migrationService->deleteM2Customer($customerId);
return response()->json($result, $result['success'] ? 200 : 400);
} catch (\Exception $e) {
Log::error('Delete M2 customer error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Deletion failed: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -123,5 +123,29 @@ public function deleteM2ProductsAboveM1Max(Request $request)
], 500);
}
}
/**
* Fix category products - ensure products are added to categories if missing from catalog_category_product table
*/
public function fixCategoryProducts(Request $request)
{
try {
$result = $this->migrationService->fixCategoryProducts();
return response()->json($result, $result['success'] ? 200 : 400);
} catch (\Exception $e) {
Log::error('Fix category products error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Fix failed: ' . $e->getMessage(),
'added' => 0,
'skipped' => 0,
'errors' => 0,
'log' => []
], 500);
}
}
}

File diff suppressed because it is too large Load Diff

115
resources/js/customers.js Normal file
View File

@ -0,0 +1,115 @@
// Customers page JavaScript
let routes = {};
let csrfToken = '';
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
if (window.customerRoutes) {
routes = window.customerRoutes;
csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
}
});
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';
const logContent = document.getElementById('customerMigrationLogContent');
logContent.innerHTML = '<div class="log-entry">' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '</div>';
fetch(routes.migrateCustomers, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ dry_run: dryRun })
})
.then(response => response.json())
.then(data => {
button.disabled = false;
otherButton.disabled = false;
button.textContent = originalText;
button.style.cursor = 'pointer';
if (data.success) {
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}`;
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('customerMigrationLogContainer');
logContainer.scrollTop = logContainer.scrollHeight;
} else {
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error');
logContent.appendChild(errorEntry);
}
})
.catch(error => {
button.disabled = false;
otherButton.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);
});
}
function deleteM2Customer(customerId, email, firstname, lastname) {
const customerName = firstname !== 'N/A' && lastname !== 'N/A'
? `${firstname} ${lastname}`
: email !== 'N/A'
? email
: `ID ${customerId}`;
if (!confirm(`Are you sure you want to delete customer "${customerName}" (Email: ${email})?`)) {
return;
}
const url = routes.deleteCustomer.replace(':id', customerId);
fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Customer deleted successfully!');
location.reload();
} else {
alert('Error: ' + (data.message || 'Failed to delete customer'));
}
})
.catch(error => {
alert('Error: ' + error.message);
});
}
// Make functions available globally
window.startCustomerMigration = startCustomerMigration;
window.deleteM2Customer = deleteM2Customer;

View File

@ -9,6 +9,12 @@ document.addEventListener('DOMContentLoaded', function() {
routes = window.productRoutes;
csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
}
// Attach event listeners to buttons
const fixCategoryProductsBtn = document.getElementById('fixCategoryProductsBtn');
if (fixCategoryProductsBtn) {
fixCategoryProductsBtn.addEventListener('click', fixCategoryProducts);
}
});
function startProductMigration(dryRun) {
@ -190,6 +196,78 @@ function syncProductCategories() {
});
}
function fixCategoryProducts() {
if (!confirm('Are you sure you want to fix category products? This will add missing products to the catalog_category_product table based on their category_ids attribute in Magento 2.')) {
return;
}
const button = document.getElementById('fixCategoryProductsBtn');
const logContainer = document.getElementById('fixCategoryProductsLogContainer');
const logContent = document.getElementById('fixCategoryProductsLogContent');
if (!button) {
console.error('fixCategoryProductsBtn not found');
return;
}
if (!routes.fixCategoryProducts) {
console.error('routes.fixCategoryProducts not found', routes);
alert('Error: Route not configured. Please refresh the page.');
return;
}
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Fixing...';
logContainer.style.display = 'block';
logContent.innerHTML = '<div class="log-entry">Starting fix process...</div>';
fetch(routes.fixCategoryProducts, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
button.disabled = false;
button.textContent = originalText;
if (data.success) {
const successEntry = document.createElement('div');
successEntry.className = 'log-entry success';
successEntry.textContent = `✓ Fix completed! Added: ${data.added || 0}, Skipped: ${data.skipped || 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' : (log.includes('ADDED') ? 'success' : 'info'));
entry.textContent = log;
logContent.appendChild(entry);
});
}
logContent.scrollTop = logContent.scrollHeight;
} else {
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ Fix failed: ' + (data.message || 'Unknown error');
logContent.appendChild(errorEntry);
}
})
.catch(error => {
button.disabled = false;
button.textContent = originalText;
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ Error: ' + error.message;
logContent.appendChild(errorEntry);
});
}
function loadM1CategoryTreeWithProducts() {
const container = document.getElementById('m1-category-products-tree-container');
if (!container) return;
@ -353,6 +431,7 @@ window.startProductMigration = startProductMigration;
window.deleteM2Product = deleteM2Product;
window.deleteProductsAboveM1Max = deleteProductsAboveM1Max;
window.syncProductCategories = syncProductCategories;
window.fixCategoryProducts = fixCategoryProducts;
window.loadM1CategoryTreeWithProducts = loadM1CategoryTreeWithProducts;
window.loadM2CategoryTreeWithProducts = loadM2CategoryTreeWithProducts;

View File

@ -0,0 +1,161 @@
@extends('layouts.app')
@section('content')
<!-- Customer Migration Section -->
<div class="section">
<h2>👥 Customer 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 Customer Migration"?</h3>
<p style="margin: 5px 0; color: #1976D2;">The customer migration process will:</p>
<ul style="margin: 10px 0 0 20px; color: #1976D2; line-height: 1.8;">
<li><strong>Create new customers:</strong> If a customer with the same email doesn't exist in Magento 2, it will be created with all its attributes</li>
<li><strong>Update existing customers:</strong> If a customer with the same email already exists in Magento 2, it will be updated with the latest data from Magento 1</li>
<li><strong>Migrate customer attributes:</strong> All customer attributes including email, firstname, lastname, and other custom attributes will be migrated</li>
<li><strong>Preserve customer data:</strong> Website ID, group ID, and timestamps will be preserved</li>
<li><strong>Generate logs:</strong> Detailed migration logs showing which customers 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="dryRunCustomerMigrationBtn" class="btn btn-secondary" onclick="startCustomerMigration(true)">
Run Dry Run (Check for Errors)
</button>
<button id="startCustomerMigrationBtn" class="btn btn-primary" onclick="startCustomerMigration(false)">
Start Customer Migration
</button>
</div>
</div>
<!-- Migration Logs Section -->
<div class="section" style="margin-top: 30px;">
<h2>📋 Customer Migration Logs</h2>
<p style="margin-bottom: 15px; color: #666;">Detailed logs showing which customers were added, updated, or encountered errors during migration:</p>
<div class="log-container" id="customerMigrationLogContainer" style="display: block;">
<div id="customerMigrationLogContent">
<div class="log-entry" style="color: #999; font-style: italic;">
No customer migration logs yet. Click "Run Dry Run" or "Start Customer Migration" to begin.
</div>
</div>
</div>
</div>
<!-- Customer Statistics -->
<div class="section" style="margin-top: 30px;">
<h2>📊 Customer Statistics</h2>
<div class="stats">
<div class="stat-card">
<div class="number">{{ $m1Customers->count() }}</div>
<div class="label">Magento 1 Customers</div>
</div>
<div class="stat-card">
<div class="number">{{ $m2Customers->count() }}</div>
<div class="label">Magento 2 Customers</div>
</div>
<div class="stat-card">
<div class="number">{{ $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="label">M2 Customers Not in M1</div>
</div>
</div>
</div>
<!-- Customers Not in M2 Section -->
@if($m1CustomersNotInM2->count() > 0)
<div class="section" style="margin-top: 30px;">
<h2>⚠️ Magento 1 Customers Not in Magento 2</h2>
<p>These customers 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>Email</th>
<th>First Name</th>
<th>Last Name</th>
<th>Website ID</th>
<th>Group ID</th>
</tr>
</thead>
<tbody>
@foreach($m1CustomersNotInM2->take(50) as $customer)
<tr>
<td>{{ $customer->entity_id }}</td>
<td>{{ $customer->email ?? 'N/A' }}</td>
<td>{{ $customer->firstname ?? 'N/A' }}</td>
<td>{{ $customer->lastname ?? 'N/A' }}</td>
<td>{{ $customer->website_id ?? 'N/A' }}</td>
<td>{{ $customer->group_id ?? 'N/A' }}</td>
</tr>
@endforeach
</tbody>
</table>
@if($m1CustomersNotInM2->count() > 50)
<p style="margin-top: 15px; color: #666;">Showing first 50 of {{ $m1CustomersNotInM2->count() }} customers.</p>
@endif
</div>
</div>
@endif
<!-- Customers Not in M1 Section -->
@if($m2CustomersNotInM1->count() > 0)
<div class="section" style="margin-top: 30px;">
<h2>⚠️ Magento 2 Customers Not in Magento 1</h2>
<p>These customers 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>Email</th>
<th>First Name</th>
<th>Last Name</th>
<th>Website ID</th>
<th>Group ID</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($m2CustomersNotInM1->take(50) as $customer)
<tr>
<td>{{ $customer->entity_id }}</td>
<td>{{ $customer->email ?? 'N/A' }}</td>
<td>{{ $customer->firstname ?? 'N/A' }}</td>
<td>{{ $customer->lastname ?? 'N/A' }}</td>
<td>{{ $customer->website_id ?? 'N/A' }}</td>
<td>{{ $customer->group_id ?? 'N/A' }}</td>
<td>
<button
class="btn btn-danger"
onclick="deleteM2Customer({{ $customer->entity_id }}, '{{ addslashes($customer->email ?? 'N/A') }}', '{{ addslashes($customer->firstname ?? 'N/A') }}', '{{ addslashes($customer->lastname ?? 'N/A') }}')"
style="padding: 6px 12px; font-size: 0.85em;">
Delete
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
@if($m2CustomersNotInM1->count() > 50)
<p style="margin-top: 15px; color: #666;">Showing first 50 of {{ $m2CustomersNotInM1->count() }} customers.</p>
@endif
</div>
</div>
@endif
@endsection
{{-- CSS is loaded globally via app.css --}}
@push('scripts')
@vite(['resources/js/customers.js'])
<script>
window.customerRoutes = {
migrateCustomers: '{{ route("customers.migrate-customers") }}',
deleteCustomer: '{{ route("customers.delete-customer", ["customerId" => ":id"]) }}'
};
</script>
@endpush

View File

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

View File

@ -185,6 +185,30 @@ class="btn btn-danger"
</button>
</div>
<!-- Fix Category Products Section -->
<div class="section" style="margin-top: 30px;">
<h2>🔧 Fix Category Products</h2>
<div class="info-box" style="margin-bottom: 20px;">
<h3 style="margin-bottom: 10px; color: #1976D2; font-size: 1.1em;">What does this do?</h3>
<p style="margin: 5px 0; color: #1976D2;">This tool checks the Magento 2 <code>catalog_category_product</code> table and ensures products are added to categories if they are missing:</p>
<ul style="margin: 10px 0 0 20px; color: #1976D2; line-height: 1.8;">
<li><strong>Finds missing products:</strong> Identifies products in Magento 2 that are not in the <code>catalog_category_product</code> table</li>
<li><strong>Checks category_ids attribute:</strong> For each missing product, reads the <code>category_ids</code> attribute value</li>
<li><strong>Adds to categories:</strong> Adds the product to the categories specified in its <code>category_ids</code> attribute</li>
<li><strong>Validates categories:</strong> Only adds products to categories that exist in Magento 2</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>
<button id="fixCategoryProductsBtn" class="btn btn-primary" style="margin-top: 15px;">
Fix Category Products
</button>
<div class="log-container" id="fixCategoryProductsLogContainer" style="display: none; margin-top: 20px; overflow: visible; max-height: none;">
<h3 style="margin-bottom: 10px; color: white;">Fix Logs</h3>
<div id="fixCategoryProductsLogContent" style="max-height: 400px; overflow-y: auto; background: #000000; color: #ffffff; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 0.9em;">
</div>
</div>
</div>
<!-- Category Tree with Products Section -->
<div class="section" style="margin-top: 30px;">
<h2>🌳 Category Tree with Products</h2>
@ -226,6 +250,7 @@ class="btn btn-danger"
deleteProduct: '{{ route("products.delete-product", ["productId" => ":id"]) }}',
deleteProductsAboveM1Max: '{{ route("products.delete-products-above-m1-max") }}',
syncProductCategories: '{{ route("products.sync-product-categories") }}',
fixCategoryProducts: '{{ route("products.fix-category-products") }}',
magento1CategoryTreeWithProducts: '{{ route("categories.magento1-category-tree-with-products") }}',
magento2CategoryTreeWithProducts: '{{ route("categories.magento2-category-tree-with-products") }}'
};

View File

@ -6,6 +6,7 @@
use App\Http\Controllers\MigrationController;
use App\Http\Controllers\AttributesController;
use App\Http\Controllers\ProductsController;
use App\Http\Controllers\CustomersController;
Route::get('/', function () {
return redirect('/connections');
@ -50,6 +51,14 @@
Route::get('/', [ProductsController::class, 'index'])->name('index');
Route::post('/migrate', [ProductsController::class, 'migrateProducts'])->name('migrate-products');
Route::post('/sync-categories', [ProductsController::class, 'syncProductCategories'])->name('sync-product-categories');
Route::post('/fix-category-products', [ProductsController::class, 'fixCategoryProducts'])->name('fix-category-products');
Route::delete('/{productId}', [ProductsController::class, 'deleteM2Product'])->name('delete-product');
Route::delete('/above-m1-max', [ProductsController::class, 'deleteM2ProductsAboveM1Max'])->name('delete-products-above-m1-max');
});
// Customers routes
Route::prefix('customers')->name('customers.')->group(function () {
Route::get('/', [CustomersController::class, 'index'])->name('index');
Route::post('/migrate', [CustomersController::class, 'migrateCustomers'])->name('migrate-customers');
Route::delete('/{customerId}', [CustomersController::class, 'deleteM2Customer'])->name('delete-customer');
});

View File

@ -13,6 +13,7 @@ export default defineConfig({
'resources/js/migration.js',
'resources/js/attributes.js',
'resources/js/products.js',
'resources/js/customers.js',
],
refresh: true,
}),