updated readme
This commit is contained in:
parent
d195fd62ab
commit
7777b6e9e7
253
README.md
253
README.md
|
|
@ -1,59 +1,236 @@
|
||||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
# Magento 1 to Magento 2 Migration Tool
|
||||||
|
|
||||||
<p align="center">
|
A comprehensive web-based migration tool built with Laravel to migrate data from Magento 1 to Magento 2, including categories, products, customers, and product options.
|
||||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## About Laravel
|
## Features
|
||||||
|
|
||||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
### 🗂️ Category Migration
|
||||||
|
- Multi-store category migration with store mapping
|
||||||
|
- Preserves category hierarchy and structure
|
||||||
|
- Migrates category attributes (name, URL key, is_active) for each store view
|
||||||
|
- Category preview before migration
|
||||||
|
- Real-time migration progress
|
||||||
|
|
||||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
### 📦 Product Migration
|
||||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
- Migrates products with all attributes
|
||||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
- Handles products with and without SKUs (auto-generates SKUs when needed)
|
||||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
- Updates existing products or creates new ones
|
||||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
- Migrates product attributes across all backend types (varchar, int, text, decimal, datetime)
|
||||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
- Product-to-category assignments
|
||||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
- Product options migration (catalog_product_option tables)
|
||||||
|
- Missing attribute detection and reporting
|
||||||
|
- Dry run mode for testing
|
||||||
|
|
||||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
### 👥 Customer Migration
|
||||||
|
- Migrates customer data with all attributes
|
||||||
|
- Creates new customers or updates existing ones based on email
|
||||||
|
- Preserves customer website and group assignments
|
||||||
|
|
||||||
## Learning Laravel
|
### 📊 Additional Features
|
||||||
|
- Web-based user interface
|
||||||
|
- Real-time migration progress tracking
|
||||||
|
- Detailed error logging (errors-only view)
|
||||||
|
- Database connection testing
|
||||||
|
- Comparison tools for products and categories
|
||||||
|
- Statistics and reporting
|
||||||
|
|
||||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
## Requirements
|
||||||
|
|
||||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
- PHP 8.1 or higher
|
||||||
|
- Laravel 11.x
|
||||||
|
- MySQL/MariaDB (for both Magento 1 and Magento 2 databases)
|
||||||
|
- Node.js and NPM (for frontend assets)
|
||||||
|
|
||||||
## Laravel Sponsors
|
## Installation
|
||||||
|
|
||||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
1. **Clone the repository:**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd migrate
|
||||||
|
```
|
||||||
|
|
||||||
### Premium Partners
|
2. **Install PHP dependencies:**
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
- **[Vehikl](https://vehikl.com)**
|
3. **Install Node dependencies:**
|
||||||
- **[Tighten Co.](https://tighten.co)**
|
```bash
|
||||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
npm install
|
||||||
- **[64 Robots](https://64robots.com)**
|
```
|
||||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
|
||||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
|
||||||
- **[Redberry](https://redberry.international/laravel-development)**
|
|
||||||
- **[Active Logic](https://activelogic.com)**
|
|
||||||
|
|
||||||
## Contributing
|
4. **Set up environment:**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
5. **Configure database connections:**
|
||||||
|
|
||||||
## Code of Conduct
|
Edit your `.env` file and add the Magento database configurations:
|
||||||
|
|
||||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
```env
|
||||||
|
# Magento 1 Database
|
||||||
|
MAGENTO1_DB_HOST=127.0.0.1
|
||||||
|
MAGENTO1_DB_PORT=3306
|
||||||
|
MAGENTO1_DB_DATABASE=magento1
|
||||||
|
MAGENTO1_DB_USERNAME=root
|
||||||
|
MAGENTO1_DB_PASSWORD=your_password
|
||||||
|
MAGENTO1_DB_PREFIX=
|
||||||
|
|
||||||
## Security Vulnerabilities
|
# Magento 2 Database
|
||||||
|
MAGENTO2_DB_HOST=127.0.0.1
|
||||||
|
MAGENTO2_DB_PORT=3306
|
||||||
|
MAGENTO2_DB_DATABASE=magento2
|
||||||
|
MAGENTO2_DB_USERNAME=root
|
||||||
|
MAGENTO2_DB_PASSWORD=your_password
|
||||||
|
MAGENTO2_DB_PREFIX=
|
||||||
|
```
|
||||||
|
|
||||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
For DDEV setups, see [DATABASE_SETUP.md](DATABASE_SETUP.md) for detailed instructions.
|
||||||
|
|
||||||
|
6. **Build frontend assets:**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Run migrations:**
|
||||||
|
```bash
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Accessing the Migration Interface
|
||||||
|
|
||||||
|
Once your Laravel application is running, navigate to:
|
||||||
|
|
||||||
|
- **Main migration page:** `/migration`
|
||||||
|
- **Products page:** `/products`
|
||||||
|
- **Categories page:** `/categories`
|
||||||
|
- **Customers page:** `/customers`
|
||||||
|
- **Attributes page:** `/attributes`
|
||||||
|
- **Connections page:** `/connections`
|
||||||
|
|
||||||
|
### Migration Workflow
|
||||||
|
|
||||||
|
1. **Test Database Connections**
|
||||||
|
- Go to the Connections page or use the "Test Connections" button
|
||||||
|
- Verify both Magento 1 and Magento 2 databases are accessible
|
||||||
|
|
||||||
|
2. **Category Migration**
|
||||||
|
- Map Magento 1 stores to Magento 2 stores
|
||||||
|
- Preview categories (optional)
|
||||||
|
- Start the migration
|
||||||
|
|
||||||
|
3. **Product Migration**
|
||||||
|
- Review missing attributes (if any)
|
||||||
|
- Run a dry run first to check for errors
|
||||||
|
- Start the actual migration
|
||||||
|
- Review error logs (only errors are shown)
|
||||||
|
|
||||||
|
4. **Customer Migration**
|
||||||
|
- Run dry run to preview changes
|
||||||
|
- Start customer migration
|
||||||
|
|
||||||
|
5. **Product Options Migration**
|
||||||
|
- Migrate catalog_product_option tables
|
||||||
|
- Includes options, prices, titles, and type values
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
⚠️ **Backup First**: Always backup your Magento 2 database before running any migration.
|
||||||
|
|
||||||
|
⚠️ **Test Environment**: It's highly recommended to test the migration on a development/staging environment first.
|
||||||
|
|
||||||
|
⚠️ **No Data Deletion**: The migration tool only performs INSERT and UPDATE operations. No data is deleted from Magento 2.
|
||||||
|
|
||||||
|
⚠️ **Products Without SKUs**: Products without SKUs will automatically receive a generated SKU in the format `MIGRATED-{entity_id}` to satisfy Magento 2's non-null SKU requirement.
|
||||||
|
|
||||||
|
⚠️ **Error Logs**: The Product Migration Logs section now shows only errors for easier troubleshooting.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── Http/Controllers/
|
||||||
|
│ ├── ProductsController.php # Product migration controller
|
||||||
|
│ ├── CategoriesController.php # Category migration controller
|
||||||
|
│ ├── CustomersController.php # Customer migration controller
|
||||||
|
│ └── ...
|
||||||
|
├── Services/
|
||||||
|
│ ├── MagentoProductMigrationService.php # Product migration logic
|
||||||
|
│ └── MagentoCategoryMigrationService.php # Category migration logic
|
||||||
|
resources/
|
||||||
|
├── views/
|
||||||
|
│ ├── products/ # Product migration interface
|
||||||
|
│ ├── categories/ # Category migration interface
|
||||||
|
│ ├── customers/ # Customer migration interface
|
||||||
|
│ └── ...
|
||||||
|
└── js/
|
||||||
|
├── products.js # Product migration frontend logic
|
||||||
|
├── categories.js # Category migration frontend logic
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [MAGENTO_MIGRATION_README.md](MAGENTO_MIGRATION_README.md) - Detailed category migration documentation
|
||||||
|
- [DATABASE_SETUP.md](DATABASE_SETUP.md) - Database connection setup for DDEV
|
||||||
|
- [ENV_SETUP_INSTRUCTIONS.md](ENV_SETUP_INSTRUCTIONS.md) - Environment configuration guide
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Errors
|
||||||
|
|
||||||
|
If you see connection errors:
|
||||||
|
|
||||||
|
1. Verify database credentials in `.env`
|
||||||
|
2. Ensure both databases are accessible from your Laravel application
|
||||||
|
3. Check database table prefixes if your Magento installations use them
|
||||||
|
4. For DDEV setups, see [DATABASE_SETUP.md](DATABASE_SETUP.md)
|
||||||
|
|
||||||
|
### Migration Errors
|
||||||
|
|
||||||
|
1. Check the migration logs in the web interface (errors-only view)
|
||||||
|
2. Review Laravel logs: `storage/logs/laravel.log`
|
||||||
|
3. Ensure Magento 2 database has proper permissions
|
||||||
|
4. Verify that required Magento 2 tables exist
|
||||||
|
5. Check for missing attributes that need to be created first
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **"Column 'sku' cannot be null"**: This has been fixed - products without SKUs now get auto-generated SKUs
|
||||||
|
- **Missing attributes**: The tool will report missing attributes that need to be created in Magento 2 first
|
||||||
|
- **Store mapping errors**: Ensure store mappings are correct before starting migration
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Assets
|
||||||
|
|
||||||
|
For development:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
For production:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
- Check Laravel logs: `storage/logs/laravel.log`
|
||||||
|
- Review migration logs in the web interface
|
||||||
|
- Check database connection status in the web interface
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,23 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\MagentoProductMigrationService;
|
||||||
use App\Services\MagentoCategoryMigrationService;
|
use App\Services\MagentoCategoryMigrationService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class ProductsController extends Controller
|
class ProductsController extends Controller
|
||||||
{
|
{
|
||||||
protected $migrationService;
|
protected $productService;
|
||||||
|
protected $categoryService;
|
||||||
|
|
||||||
public function __construct(MagentoCategoryMigrationService $migrationService)
|
public function __construct(MagentoProductMigrationService $productService, MagentoCategoryMigrationService $categoryService)
|
||||||
{
|
{
|
||||||
$this->migrationService = $migrationService;
|
$this->productService = $productService;
|
||||||
|
$this->categoryService = $categoryService;
|
||||||
|
// Inject category service into product service
|
||||||
|
$this->productService->setCategoryService($categoryService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,10 +26,10 @@ public function __construct(MagentoCategoryMigrationService $migrationService)
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$m1Products = $this->migrationService->getMagento1Products();
|
$m1Products = $this->productService->getMagento1Products();
|
||||||
$m2Products = $this->migrationService->getMagento2Products();
|
$m2Products = $this->productService->getMagento2Products();
|
||||||
$m1ProductsNotInM2 = $this->migrationService->getM1ProductsNotInM2();
|
$m1ProductsNotInM2 = $this->productService->getM1ProductsNotInM2();
|
||||||
$m2ProductsNotInM1 = $this->migrationService->getM2ProductsNotInM1();
|
$m2ProductsNotInM1 = $this->productService->getM2ProductsNotInM1();
|
||||||
|
|
||||||
return view('products.index', [
|
return view('products.index', [
|
||||||
'm1Products' => $m1Products,
|
'm1Products' => $m1Products,
|
||||||
|
|
@ -40,7 +46,19 @@ public function migrateProducts(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$dryRun = $request->input('dry_run', false);
|
$dryRun = $request->input('dry_run', false);
|
||||||
$result = $this->migrationService->migrateProducts($dryRun);
|
|
||||||
|
// Get progress key from request or generate one (only for non-dry-run)
|
||||||
|
$progressKey = $request->input('progress_key');
|
||||||
|
if (!$dryRun && !$progressKey) {
|
||||||
|
$progressKey = 'product_migration_progress_' . uniqid();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->productService->migrateProducts($dryRun, $progressKey);
|
||||||
|
|
||||||
|
// Add progress key to result
|
||||||
|
if ($progressKey) {
|
||||||
|
$result['progress_key'] = $progressKey;
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json($result, $result['success'] ? 200 : 400);
|
return response()->json($result, $result['success'] ? 200 : 400);
|
||||||
|
|
||||||
|
|
@ -65,7 +83,7 @@ public function migrateProducts(Request $request)
|
||||||
public function deleteM2Product(Request $request, $productId)
|
public function deleteM2Product(Request $request, $productId)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$result = $this->migrationService->deleteM2Product($productId);
|
$result = $this->productService->deleteM2Product($productId);
|
||||||
|
|
||||||
return response()->json($result, $result['success'] ? 200 : 400);
|
return response()->json($result, $result['success'] ? 200 : 400);
|
||||||
|
|
||||||
|
|
@ -85,7 +103,7 @@ public function deleteM2Product(Request $request, $productId)
|
||||||
public function syncProductCategories(Request $request)
|
public function syncProductCategories(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$result = $this->migrationService->syncProductCategories();
|
$result = $this->productService->syncProductCategories();
|
||||||
|
|
||||||
return response()->json($result, $result['success'] ? 200 : 400);
|
return response()->json($result, $result['success'] ? 200 : 400);
|
||||||
|
|
||||||
|
|
@ -109,7 +127,7 @@ public function syncProductCategories(Request $request)
|
||||||
public function deleteM2ProductsAboveM1Max(Request $request)
|
public function deleteM2ProductsAboveM1Max(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$result = $this->migrationService->deleteM2ProductsAboveM1Max();
|
$result = $this->productService->deleteM2ProductsAboveM1Max();
|
||||||
|
|
||||||
return response()->json($result, $result['success'] ? 200 : 400);
|
return response()->json($result, $result['success'] ? 200 : 400);
|
||||||
|
|
||||||
|
|
@ -130,7 +148,7 @@ public function deleteM2ProductsAboveM1Max(Request $request)
|
||||||
public function fixCategoryProducts(Request $request)
|
public function fixCategoryProducts(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$result = $this->migrationService->fixCategoryProducts();
|
$result = $this->productService->fixCategoryProducts();
|
||||||
|
|
||||||
return response()->json($result, $result['success'] ? 200 : 400);
|
return response()->json($result, $result['success'] ? 200 : 400);
|
||||||
|
|
||||||
|
|
@ -155,7 +173,7 @@ public function migrateProductOptions(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$dryRun = $request->input('dry_run', false);
|
$dryRun = $request->input('dry_run', false);
|
||||||
$result = $this->migrationService->migrateProductOptions($dryRun);
|
$result = $this->productService->migrateProductOptions($dryRun);
|
||||||
|
|
||||||
return response()->json($result, $result['success'] ? 200 : 400);
|
return response()->json($result, $result['success'] ? 200 : 400);
|
||||||
|
|
||||||
|
|
@ -172,5 +190,76 @@ public function migrateProductOptions(Request $request)
|
||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare M1 and M2 product values by SKU
|
||||||
|
*/
|
||||||
|
public function compareProductBySku(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$sku = $request->input('sku');
|
||||||
|
|
||||||
|
if (empty($sku)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'SKU is required',
|
||||||
|
'changes' => []
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->productService->compareProductBySku($sku);
|
||||||
|
|
||||||
|
return response()->json($result, $result['success'] ? 200 : 400);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Product comparison error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Comparison failed: ' . $e->getMessage(),
|
||||||
|
'changes' => []
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get 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 migration progress error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to get progress: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1965,6 +1965,16 @@ public function getM2ProductsNotInM1()
|
||||||
/**
|
/**
|
||||||
* Migrate all products from Magento 1 to Magento 2
|
* Migrate all products from Magento 1 to Magento 2
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Migrate all products from Magento 1 to Magento 2
|
||||||
|
*
|
||||||
|
* IMPORTANT: This migration only performs INSERT and UPDATE operations.
|
||||||
|
* NO DATA IS DELETED during migration. Existing data in M2 that doesn't
|
||||||
|
* exist in M1 will be preserved and not removed.
|
||||||
|
*
|
||||||
|
* @param bool $dryRun If true, only simulates the migration without making changes
|
||||||
|
* @return array Migration result with counts and logs
|
||||||
|
*/
|
||||||
public function migrateProducts($dryRun = false)
|
public function migrateProducts($dryRun = false)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -2166,15 +2176,30 @@ public function migrateProducts($dryRun = false)
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// If SKU column exists, set it to NULL or empty
|
// If SKU column exists, generate a SKU for products without one
|
||||||
|
$generatedSku = 'MIGRATED-' . $m2ProductId;
|
||||||
if ($m2HasSkuColumn) {
|
if ($m2HasSkuColumn) {
|
||||||
$insertData['sku'] = null;
|
// Generate a unique SKU using entity_id to avoid null constraint violation
|
||||||
|
$insertData['sku'] = $generatedSku;
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::connection($this->magento2Connection)
|
DB::connection($this->magento2Connection)
|
||||||
->table($this->magento2Prefix . 'catalog_product_entity')
|
->table($this->magento2Prefix . 'catalog_product_entity')
|
||||||
->insert($insertData);
|
->insert($insertData);
|
||||||
$this->migrationLog[] = "Created new product: No SKU (ID: {$m2ProductId}, using M1 ID)";
|
|
||||||
|
// If SKU is stored in EAV table, insert it there too
|
||||||
|
if (!$m2HasSkuColumn && isset($m2AttributeIds['sku'])) {
|
||||||
|
DB::connection($this->magento2Connection)
|
||||||
|
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
|
||||||
|
->insert([
|
||||||
|
'attribute_id' => $m2AttributeIds['sku'],
|
||||||
|
'store_id' => 0,
|
||||||
|
'entity_id' => $m2ProductId,
|
||||||
|
'value' => $generatedSku,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->migrationLog[] = "Created new product: Generated SKU {$generatedSku} (ID: {$m2ProductId}, using M1 ID)";
|
||||||
} else {
|
} else {
|
||||||
$this->migrationLog[] = "Updating existing product: No SKU (ID: {$m2ProductId})";
|
$this->migrationLog[] = "Updating existing product: No SKU (ID: {$m2ProductId})";
|
||||||
$updatedCount++;
|
$updatedCount++;
|
||||||
|
|
@ -2424,6 +2449,10 @@ protected function getMagento2ProductAttributeIds()
|
||||||
/**
|
/**
|
||||||
* Migrate ALL data from ALL catalog_product_entity_* tables for a product
|
* Migrate ALL data from ALL catalog_product_entity_* tables for a product
|
||||||
* This ensures we migrate images, all store views, and all attributes
|
* This ensures we migrate images, all store views, and all attributes
|
||||||
|
*
|
||||||
|
* IMPORTANT: This method only performs INSERT and UPDATE operations.
|
||||||
|
* NO DATA IS DELETED. Existing attribute values in M2 that don't exist
|
||||||
|
* in M1 will be preserved and not removed.
|
||||||
*/
|
*/
|
||||||
protected function migrateAllProductEntityData($m1ProductId, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $m1AttributeIds, $m2AttributeIds, $dryRun = false, $allM1Attributes = null)
|
protected function migrateAllProductEntityData($m1ProductId, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $m1AttributeIds, $m2AttributeIds, $dryRun = false, $allM1Attributes = null)
|
||||||
{
|
{
|
||||||
|
|
@ -3050,6 +3079,282 @@ protected function migrateProductAttributes($m1ProductId, $m2ProductId, $m1Entit
|
||||||
return $missingAttributes;
|
return $missingAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare M1 and M2 product values by SKU
|
||||||
|
* Returns all differences that would be updated during migration
|
||||||
|
*/
|
||||||
|
public function compareProductBySku($sku)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$comparisonByTable = [];
|
||||||
|
|
||||||
|
// Get M1 and M2 entity type IDs
|
||||||
|
$m1EntityTypeId = DB::connection($this->magento1Connection)
|
||||||
|
->table($this->magento1Prefix . 'eav_entity_type')
|
||||||
|
->where('entity_type_code', 'catalog_product')
|
||||||
|
->value('entity_type_id');
|
||||||
|
|
||||||
|
$m2EntityTypeId = DB::connection($this->magento2Connection)
|
||||||
|
->table($this->magento2Prefix . 'eav_entity_type')
|
||||||
|
->where('entity_type_code', 'catalog_product')
|
||||||
|
->value('entity_type_id');
|
||||||
|
|
||||||
|
if (!$m1EntityTypeId || !$m2EntityTypeId) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Entity type not found',
|
||||||
|
'changes_by_table' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find M1 product by SKU
|
||||||
|
$m1Product = $this->findProductBySku($sku, $this->magento1Connection, $this->magento1Prefix);
|
||||||
|
if (!$m1Product) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => "Product with SKU '{$sku}' not found in Magento 1",
|
||||||
|
'changes_by_table' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$m1ProductId = $m1Product->entity_id;
|
||||||
|
|
||||||
|
// Find M2 product by SKU
|
||||||
|
$m2Product = $this->findProductBySku($sku, $this->magento2Connection, $this->magento2Prefix);
|
||||||
|
if (!$m2Product) {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Product with SKU '{$sku}' not found in Magento 2. It will be created during migration.",
|
||||||
|
'm1_product_id' => $m1ProductId,
|
||||||
|
'm2_product_id' => null,
|
||||||
|
'changes_by_table' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$m2ProductId = $m2Product->entity_id;
|
||||||
|
|
||||||
|
// Get all M1 attributes
|
||||||
|
$allM1Attributes = $this->getAllMagento1ProductAttributes();
|
||||||
|
$m2AttributeIds = $this->getAllMagento2ProductAttributeIds();
|
||||||
|
|
||||||
|
// Build attribute mapping
|
||||||
|
$attributeMapping = [];
|
||||||
|
if ($allM1Attributes) {
|
||||||
|
foreach ($allM1Attributes as $m1Attr) {
|
||||||
|
$attrCode = $m1Attr->attribute_code;
|
||||||
|
if (isset($m2AttributeIds[$attrCode])) {
|
||||||
|
$attributeMapping[$m1Attr->attribute_id] = [
|
||||||
|
'm2_attr_id' => $m2AttributeIds[$attrCode],
|
||||||
|
'backend_type' => $m1Attr->backend_type ?? 'varchar',
|
||||||
|
'attribute_code' => $attrCode,
|
||||||
|
'frontend_label' => $m1Attr->frontend_label ?? $attrCode
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare all backend type tables
|
||||||
|
$backendTypes = ['varchar', 'int', 'text', 'decimal', 'datetime'];
|
||||||
|
|
||||||
|
foreach ($backendTypes as $backendType) {
|
||||||
|
$m1Table = $this->magento1Prefix . 'catalog_product_entity_' . $backendType;
|
||||||
|
$m2Table = $this->magento2Prefix . 'catalog_product_entity_' . $backendType;
|
||||||
|
$tableChanges = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get M1 values
|
||||||
|
$m1Rows = DB::connection($this->magento1Connection)
|
||||||
|
->table($m1Table)
|
||||||
|
->where('entity_id', $m1ProductId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Get M2 values
|
||||||
|
$m2Rows = DB::connection($this->magento2Connection)
|
||||||
|
->table($m2Table)
|
||||||
|
->where('entity_id', $m2ProductId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Build M2 lookup by attribute_id and store_id
|
||||||
|
$m2Lookup = [];
|
||||||
|
foreach ($m2Rows as $m2Row) {
|
||||||
|
$key = $m2Row->attribute_id . '_' . ($m2Row->store_id ?? 0);
|
||||||
|
$m2Lookup[$key] = $m2Row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare M1 values with M2
|
||||||
|
foreach ($m1Rows as $m1Row) {
|
||||||
|
$m1AttrId = $m1Row->attribute_id;
|
||||||
|
|
||||||
|
// Check if attribute exists in M2
|
||||||
|
if (!isset($attributeMapping[$m1AttrId])) {
|
||||||
|
continue; // Skip attributes that don't exist in M2
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapping = $attributeMapping[$m1AttrId];
|
||||||
|
$m2AttrId = $mapping['m2_attr_id'];
|
||||||
|
$storeId = $m1Row->store_id ?? 0;
|
||||||
|
$key = $m2AttrId . '_' . $storeId;
|
||||||
|
|
||||||
|
$m1Value = $m1Row->value ?? '';
|
||||||
|
$m2Value = isset($m2Lookup[$key]) ? ($m2Lookup[$key]->value ?? '') : null;
|
||||||
|
|
||||||
|
// Check if values are different
|
||||||
|
if ($m1Value != $m2Value) {
|
||||||
|
$tableChanges[] = [
|
||||||
|
'attribute_code' => $mapping['attribute_code'],
|
||||||
|
'attribute_label' => $mapping['frontend_label'],
|
||||||
|
'backend_type' => $mapping['backend_type'],
|
||||||
|
'store_id' => $storeId,
|
||||||
|
'm1_value' => $m1Value,
|
||||||
|
'm2_value' => $m2Value,
|
||||||
|
'action' => $m2Value === null ? 'INSERT' : 'UPDATE'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for M2 values that don't exist in M1 (would be deleted)
|
||||||
|
foreach ($m2Rows as $m2Row) {
|
||||||
|
$m2AttrId = $m2Row->attribute_id;
|
||||||
|
$storeId = $m2Row->store_id ?? 0;
|
||||||
|
|
||||||
|
// Check if this attribute exists in M1 mapping
|
||||||
|
$foundInM1 = false;
|
||||||
|
foreach ($attributeMapping as $m1AttrId => $mapping) {
|
||||||
|
if ($mapping['m2_attr_id'] == $m2AttrId) {
|
||||||
|
// Check if M1 has this value
|
||||||
|
$m1HasValue = $m1Rows->where('attribute_id', $m1AttrId)
|
||||||
|
->where('store_id', $storeId)
|
||||||
|
->isNotEmpty();
|
||||||
|
if ($m1HasValue) {
|
||||||
|
$foundInM1 = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in M1 and it's not a system attribute, note it
|
||||||
|
if (!$foundInM1) {
|
||||||
|
// Try to get attribute code for display
|
||||||
|
$attrCode = 'unknown';
|
||||||
|
foreach ($attributeMapping as $m1AttrId => $mapping) {
|
||||||
|
if ($mapping['m2_attr_id'] == $m2AttrId) {
|
||||||
|
$attrCode = $mapping['attribute_code'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add if it's a meaningful attribute (not system-generated)
|
||||||
|
if ($attrCode !== 'unknown') {
|
||||||
|
$tableChanges[] = [
|
||||||
|
'attribute_code' => $attrCode,
|
||||||
|
'attribute_label' => 'Unknown',
|
||||||
|
'backend_type' => $backendType,
|
||||||
|
'store_id' => $storeId,
|
||||||
|
'm1_value' => null,
|
||||||
|
'm2_value' => $m2Row->value ?? '',
|
||||||
|
'action' => 'DELETE'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add table changes to comparison (even if empty, to show all tables)
|
||||||
|
$comparisonByTable[$backendType] = [
|
||||||
|
'table_name' => 'catalog_product_entity_' . $backendType,
|
||||||
|
'm1_table' => $m1Table,
|
||||||
|
'm2_table' => $m2Table,
|
||||||
|
'changes' => $tableChanges
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Table might not exist, add empty entry
|
||||||
|
$comparisonByTable[$backendType] = [
|
||||||
|
'table_name' => 'catalog_product_entity_' . $backendType,
|
||||||
|
'm1_table' => $m1Table,
|
||||||
|
'm2_table' => $m2Table,
|
||||||
|
'changes' => [],
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
Log::debug("Table comparison error for {$m1Table}: " . $e->getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Comparison completed for SKU '{$sku}'",
|
||||||
|
'm1_product_id' => $m1ProductId,
|
||||||
|
'm2_product_id' => $m2ProductId,
|
||||||
|
'sku' => $sku,
|
||||||
|
'changes_by_table' => $comparisonByTable
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error comparing product by SKU: ' . $e->getMessage());
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Comparison failed: ' . $e->getMessage(),
|
||||||
|
'changes' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find product by SKU in either M1 or M2
|
||||||
|
*/
|
||||||
|
protected function findProductBySku($sku, $connection, $prefix)
|
||||||
|
{
|
||||||
|
// Try entity table first
|
||||||
|
try {
|
||||||
|
$product = DB::connection($connection)
|
||||||
|
->table($prefix . 'catalog_product_entity')
|
||||||
|
->where('sku', $sku)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($product) {
|
||||||
|
return $product;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// SKU might be in EAV table
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try EAV table
|
||||||
|
$entityTypeId = DB::connection($connection)
|
||||||
|
->table($prefix . 'eav_entity_type')
|
||||||
|
->where('entity_type_code', 'catalog_product')
|
||||||
|
->value('entity_type_id');
|
||||||
|
|
||||||
|
if (!$entityTypeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$skuAttributeId = DB::connection($connection)
|
||||||
|
->table($prefix . 'eav_attribute')
|
||||||
|
->where('entity_type_id', $entityTypeId)
|
||||||
|
->where('attribute_code', 'sku')
|
||||||
|
->value('attribute_id');
|
||||||
|
|
||||||
|
if (!$skuAttributeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$skuRow = DB::connection($connection)
|
||||||
|
->table($prefix . 'catalog_product_entity_varchar')
|
||||||
|
->where('attribute_id', $skuAttributeId)
|
||||||
|
->where('value', $sku)
|
||||||
|
->where('store_id', 0)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$skuRow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::connection($connection)
|
||||||
|
->table($prefix . 'catalog_product_entity')
|
||||||
|
->where('entity_id', $skuRow->entity_id)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a single product from Magento 2
|
* Delete a single product from Magento 2
|
||||||
*/
|
*/
|
||||||
|
|
@ -3452,6 +3757,9 @@ public function deleteM2ProductsAboveM1Max()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate product category associations
|
* Migrate product category associations
|
||||||
|
*
|
||||||
|
* IMPORTANT: This method only performs INSERT operations.
|
||||||
|
* NO DATA IS DELETED. Existing category associations in M2 will be preserved.
|
||||||
*/
|
*/
|
||||||
protected function migrateProductCategories($m1ProductId, $m2ProductId, $categoryMapping)
|
protected function migrateProductCategories($m1ProductId, $m2ProductId, $categoryMapping)
|
||||||
{
|
{
|
||||||
|
|
@ -3489,7 +3797,7 @@ protected function migrateProductCategories($m1ProductId, $m2ProductId, $categor
|
||||||
/**
|
/**
|
||||||
* Get store mapping by matching store codes
|
* Get store mapping by matching store codes
|
||||||
*/
|
*/
|
||||||
protected function getStoreMapping()
|
public function getStoreMapping()
|
||||||
{
|
{
|
||||||
// If store mapping was set during category migration, use it
|
// If store mapping was set during category migration, use it
|
||||||
if (!empty($this->storeMapping)) {
|
if (!empty($this->storeMapping)) {
|
||||||
|
|
@ -3600,6 +3908,9 @@ protected function getWebsiteMapping()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate product website assignments from M1 catalog_product_website to M2 catalog_product_website
|
* Migrate product website assignments from M1 catalog_product_website to M2 catalog_product_website
|
||||||
|
*
|
||||||
|
* IMPORTANT: This method only performs INSERT operations.
|
||||||
|
* NO DATA IS DELETED. Existing website assignments in M2 will be preserved.
|
||||||
*/
|
*/
|
||||||
protected function migrateProductWebsites($m1ProductId, $m2ProductId)
|
protected function migrateProductWebsites($m1ProductId, $m2ProductId)
|
||||||
{
|
{
|
||||||
|
|
@ -3754,6 +4065,12 @@ protected function migrateProductUrlRewrites($m1ProductId, $m2ProductId)
|
||||||
/**
|
/**
|
||||||
* Migrate stock inventory from M1 cataloginventory_stock_item to M2 cataloginventory_stock_item
|
* Migrate stock inventory from M1 cataloginventory_stock_item to M2 cataloginventory_stock_item
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Migrate product stock inventory from M1 to M2
|
||||||
|
*
|
||||||
|
* IMPORTANT: This method only performs INSERT and UPDATE operations.
|
||||||
|
* NO DATA IS DELETED. Existing stock data in M2 will be preserved or updated.
|
||||||
|
*/
|
||||||
protected function migrateProductStock($m1ProductId, $m2ProductId)
|
protected function migrateProductStock($m1ProductId, $m2ProductId)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -3909,6 +4226,12 @@ protected function createDefaultStockItem($productId)
|
||||||
/**
|
/**
|
||||||
* Migrate all catalog_product_option tables from M1 to M2
|
* Migrate all catalog_product_option tables from M1 to M2
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Migrate catalog_product_option tables from M1 to M2
|
||||||
|
*
|
||||||
|
* IMPORTANT: This method only performs INSERT and UPDATE operations.
|
||||||
|
* NO DATA IS DELETED. Existing product options in M2 will be preserved or updated.
|
||||||
|
*/
|
||||||
protected function migrateCatalogProductOptions()
|
protected function migrateCatalogProductOptions()
|
||||||
{
|
{
|
||||||
$migratedCount = 0;
|
$migratedCount = 0;
|
||||||
|
|
@ -4809,7 +5132,7 @@ protected function findCategoryByName($categoryName, $source = 'm2')
|
||||||
/**
|
/**
|
||||||
* Get category mapping from previous migrations
|
* Get category mapping from previous migrations
|
||||||
*/
|
*/
|
||||||
protected function getCategoryMapping()
|
public function getCategoryMapping()
|
||||||
{
|
{
|
||||||
// Try to get mapping from category_mapping table or build it from existing categories
|
// Try to get mapping from category_mapping table or build it from existing categories
|
||||||
// Build it by matching category names (case-insensitive, ignoring parent differences)
|
// Build it by matching category names (case-insensitive, ignoring parent differences)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -43,6 +43,8 @@ function startProductMigration(dryRun) {
|
||||||
button.style.cursor = 'not-allowed';
|
button.style.cursor = 'not-allowed';
|
||||||
|
|
||||||
const logContent = document.getElementById('productMigrationLogContent');
|
const logContent = document.getElementById('productMigrationLogContent');
|
||||||
|
const progressContainer = document.getElementById('migrationProgressContainer');
|
||||||
|
|
||||||
if (!logContent) {
|
if (!logContent) {
|
||||||
console.error('Log content element not found');
|
console.error('Log content element not found');
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
|
@ -53,36 +55,198 @@ function startProductMigration(dryRun) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show progress bar
|
||||||
|
if (progressContainer && !dryRun) {
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
updateProgressBar(0, 0, 0, 0, 0, 'Starting migration...', '');
|
||||||
|
}
|
||||||
|
|
||||||
logContent.innerHTML = '<div class="log-entry">' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '</div>';
|
logContent.innerHTML = '<div class="log-entry">' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '</div>';
|
||||||
|
|
||||||
|
// Generate progress key for tracking (only for non-dry-run)
|
||||||
|
let progressKey = null;
|
||||||
|
let progressInterval = null;
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
progressKey = 'product_migration_progress_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
// Start polling immediately
|
||||||
|
progressInterval = pollMigrationProgress(progressKey, button, otherButton, originalText, logContent);
|
||||||
|
}
|
||||||
|
|
||||||
fetch(routes.migrateProducts, {
|
fetch(routes.migrateProducts, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRF-TOKEN': csrfToken
|
'X-CSRF-TOKEN': csrfToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ dry_run: dryRun })
|
body: JSON.stringify({ dry_run: dryRun, progress_key: progressKey })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
// Stop polling when migration completes
|
||||||
|
if (progressInterval) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle final result
|
||||||
|
handleMigrationComplete(data, button, otherButton, originalText, logContent, dryRun);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Stop polling on error
|
||||||
|
if (progressInterval) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
}
|
||||||
|
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
if (otherButton) {
|
||||||
otherButton.disabled = false;
|
otherButton.disabled = false;
|
||||||
|
}
|
||||||
button.textContent = originalText;
|
button.textContent = originalText;
|
||||||
button.style.cursor = 'pointer';
|
button.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
const progressContainer = document.getElementById('migrationProgressContainer');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorEntry = document.createElement('div');
|
||||||
|
errorEntry.className = 'log-entry error';
|
||||||
|
errorEntry.textContent = '✗ Error: ' + error.message;
|
||||||
|
logContent.appendChild(errorEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollMigrationProgress(progressKey, button, otherButton, 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_sku
|
||||||
|
);
|
||||||
|
} 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, currentSku) {
|
||||||
|
const progressBar = document.getElementById('progressBar');
|
||||||
|
const progressPercentage = document.getElementById('progressPercentage');
|
||||||
|
const progressStatus = document.getElementById('progressStatus');
|
||||||
|
const currentProduct = document.getElementById('currentProduct');
|
||||||
|
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 (currentProduct) {
|
||||||
|
currentProduct.textContent = currentSku ? `Current: ${currentSku}` : `Processing ${current} of ${total}`;
|
||||||
|
}
|
||||||
|
if (progressAdded) {
|
||||||
|
progressAdded.textContent = added;
|
||||||
|
}
|
||||||
|
if (progressUpdated) {
|
||||||
|
progressUpdated.textContent = updated;
|
||||||
|
}
|
||||||
|
if (progressErrors) {
|
||||||
|
progressErrors.textContent = errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMigrationComplete(data, button, otherButton, originalText, logContent, dryRun) {
|
||||||
|
button.disabled = false;
|
||||||
|
if (otherButton) {
|
||||||
|
otherButton.disabled = false;
|
||||||
|
}
|
||||||
|
button.textContent = originalText;
|
||||||
|
button.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
const progressContainer = document.getElementById('migrationProgressContainer');
|
||||||
|
if (progressContainer) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const successEntry = document.createElement('div');
|
updateProgressBar(100, data.added + data.updated, data.added + data.updated, data.added, data.updated, data.errors, 'completed', '');
|
||||||
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.success) {
|
||||||
|
// Clear log content and show only errors
|
||||||
|
logContent.innerHTML = '';
|
||||||
|
|
||||||
if (data.log && data.log.length > 0) {
|
if (data.log && data.log.length > 0) {
|
||||||
data.log.forEach(log => {
|
// Filter to show only error logs (case-insensitive check)
|
||||||
|
const errorLogs = data.log.filter(log => {
|
||||||
|
const logUpper = log.toUpperCase();
|
||||||
|
return logUpper.includes('ERROR');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorLogs.length > 0) {
|
||||||
|
// Show summary with error count
|
||||||
|
const summaryEntry = document.createElement('div');
|
||||||
|
summaryEntry.className = 'log-entry';
|
||||||
|
summaryEntry.style.cssText = 'font-weight: 600; margin-bottom: 10px; color: #dc3545;';
|
||||||
|
summaryEntry.textContent = `⚠️ Migration completed with ${errorLogs.length} error(s). Added: ${data.added || 0}, Updated: ${data.updated || 0}`;
|
||||||
|
logContent.appendChild(summaryEntry);
|
||||||
|
|
||||||
|
// Show each error
|
||||||
|
errorLogs.forEach(log => {
|
||||||
const entry = document.createElement('div');
|
const entry = document.createElement('div');
|
||||||
entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : 'success');
|
entry.className = 'log-entry error';
|
||||||
entry.textContent = log;
|
entry.textContent = log;
|
||||||
logContent.appendChild(entry);
|
logContent.appendChild(entry);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// No errors found
|
||||||
|
const noErrorsEntry = document.createElement('div');
|
||||||
|
noErrorsEntry.className = 'log-entry';
|
||||||
|
noErrorsEntry.style.cssText = 'color: #28a745; font-weight: 600;';
|
||||||
|
noErrorsEntry.textContent = `✓ ${dryRun ? 'Dry run' : 'Migration'} completed successfully! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: 0`;
|
||||||
|
logContent.appendChild(noErrorsEntry);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No logs at all
|
||||||
|
const noLogsEntry = document.createElement('div');
|
||||||
|
noLogsEntry.className = 'log-entry';
|
||||||
|
noLogsEntry.style.cssText = 'color: #28a745; font-weight: 600;';
|
||||||
|
noLogsEntry.textContent = `✓ ${dryRun ? 'Dry run' : 'Migration'} completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`;
|
||||||
|
logContent.appendChild(noLogsEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.missing_attributes && data.missing_attributes.length > 0) {
|
if (data.missing_attributes && data.missing_attributes.length > 0) {
|
||||||
|
|
@ -109,18 +273,6 @@ function startProductMigration(dryRun) {
|
||||||
errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error');
|
errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error');
|
||||||
logContent.appendChild(errorEntry);
|
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 deleteM2Product(productId, productName, productSku) {
|
function deleteM2Product(productId, productName, productSku) {
|
||||||
|
|
@ -546,6 +698,254 @@ function startProductOptionsMigration(dryRun) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareProductBySku() {
|
||||||
|
const skuInput = document.getElementById('productSkuInput');
|
||||||
|
const compareBtn = document.getElementById('compareProductBtn');
|
||||||
|
const comparisonSection = document.getElementById('productComparisonSection');
|
||||||
|
const comparisonMessage = document.getElementById('productComparisonMessage');
|
||||||
|
|
||||||
|
if (!skuInput || !compareBtn || !comparisonSection) {
|
||||||
|
console.error('Required elements not found');
|
||||||
|
alert('Error: Required elements not found. Please refresh the page.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sku = skuInput.value.trim();
|
||||||
|
|
||||||
|
if (!sku) {
|
||||||
|
alert('Please enter a product SKU');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!routes || !routes.compareProductBySku) {
|
||||||
|
console.error('Route not available', routes);
|
||||||
|
alert('Error: Route not configured. Please refresh the page.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalText = compareBtn.textContent;
|
||||||
|
compareBtn.disabled = true;
|
||||||
|
compareBtn.textContent = 'Comparing...';
|
||||||
|
comparisonSection.style.display = 'none';
|
||||||
|
|
||||||
|
fetch(routes.compareProductBySku, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ sku: sku })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
compareBtn.disabled = false;
|
||||||
|
compareBtn.textContent = originalText;
|
||||||
|
comparisonSection.style.display = 'block';
|
||||||
|
|
||||||
|
const tablesContainer = document.getElementById('productComparisonTablesContainer');
|
||||||
|
if (!tablesContainer) {
|
||||||
|
console.error('Tables container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear previous results
|
||||||
|
tablesContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Display message
|
||||||
|
comparisonMessage.textContent = data.message || 'Comparison completed';
|
||||||
|
comparisonMessage.style.background = '#d4edda';
|
||||||
|
comparisonMessage.style.color = '#155724';
|
||||||
|
comparisonMessage.style.border = '1px solid #c3e6cb';
|
||||||
|
|
||||||
|
if (data.m1_product_id) {
|
||||||
|
const infoText = document.createElement('div');
|
||||||
|
infoText.style.marginTop = '5px';
|
||||||
|
infoText.style.fontSize = '0.9em';
|
||||||
|
infoText.textContent = `M1 Product ID: ${data.m1_product_id}${data.m2_product_id ? ` | M2 Product ID: ${data.m2_product_id}` : ''}`;
|
||||||
|
comparisonMessage.appendChild(infoText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display tables for each backend type
|
||||||
|
if (data.changes_by_table) {
|
||||||
|
const backendTypes = ['varchar', 'int', 'text', 'decimal', 'datetime'];
|
||||||
|
let hasAnyChanges = false;
|
||||||
|
|
||||||
|
backendTypes.forEach(backendType => {
|
||||||
|
const tableData = data.changes_by_table[backendType];
|
||||||
|
if (!tableData) return;
|
||||||
|
|
||||||
|
const changes = tableData.changes || [];
|
||||||
|
if (changes.length > 0) {
|
||||||
|
hasAnyChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table container
|
||||||
|
const tableWrapper = document.createElement('div');
|
||||||
|
tableWrapper.style.cssText = 'background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; margin-bottom: 20px; max-height: 600px; overflow-y: auto;';
|
||||||
|
|
||||||
|
// Table title
|
||||||
|
const tableTitle = document.createElement('h4');
|
||||||
|
tableTitle.style.cssText = 'margin: 0 0 15px 0; color: #333; font-size: 1.1em;';
|
||||||
|
tableTitle.textContent = `Table: ${tableData.table_name}`;
|
||||||
|
tableWrapper.appendChild(tableTitle);
|
||||||
|
|
||||||
|
// Table info
|
||||||
|
const tableInfo = document.createElement('div');
|
||||||
|
tableInfo.style.cssText = 'margin-bottom: 15px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 0.9em; color: #666;';
|
||||||
|
tableInfo.innerHTML = `
|
||||||
|
<strong>M1 Table:</strong> ${tableData.m1_table}<br>
|
||||||
|
<strong>M2 Table:</strong> ${tableData.m2_table}<br>
|
||||||
|
<strong>Changes:</strong> ${changes.length}
|
||||||
|
`;
|
||||||
|
tableWrapper.appendChild(tableInfo);
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.className = 'products-table';
|
||||||
|
table.style.width = '100%';
|
||||||
|
|
||||||
|
// Table header
|
||||||
|
const thead = document.createElement('thead');
|
||||||
|
thead.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<th>Attribute Code</th>
|
||||||
|
<th>Attribute Label</th>
|
||||||
|
<th>Store ID</th>
|
||||||
|
<th>M1 Value (To Update)</th>
|
||||||
|
<th>M2 Current Value</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
table.appendChild(thead);
|
||||||
|
|
||||||
|
// Table body
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
|
||||||
|
if (changes.length > 0) {
|
||||||
|
changes.forEach(change => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
// Determine row style based on action
|
||||||
|
if (change.action === 'INSERT') {
|
||||||
|
row.style.backgroundColor = '#d4edda';
|
||||||
|
} else if (change.action === 'UPDATE') {
|
||||||
|
row.style.backgroundColor = '#fff3cd';
|
||||||
|
} else if (change.action === 'DELETE') {
|
||||||
|
row.style.backgroundColor = '#f8d7da';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format values for display
|
||||||
|
const m1Value = change.m1_value !== null && change.m1_value !== undefined
|
||||||
|
? (change.m1_value.toString().length > 100
|
||||||
|
? change.m1_value.toString().substring(0, 100) + '...'
|
||||||
|
: change.m1_value.toString())
|
||||||
|
: '(empty)';
|
||||||
|
|
||||||
|
const m2Value = change.m2_value !== null && change.m2_value !== undefined
|
||||||
|
? (change.m2_value.toString().length > 100
|
||||||
|
? change.m2_value.toString().substring(0, 100) + '...'
|
||||||
|
: change.m2_value.toString())
|
||||||
|
: '(empty)';
|
||||||
|
|
||||||
|
// Determine action badge color
|
||||||
|
let actionBadgeClass = '';
|
||||||
|
let actionText = change.action;
|
||||||
|
if (change.action === 'INSERT') {
|
||||||
|
actionBadgeClass = 'style="background-color: #28a745; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;"';
|
||||||
|
} else if (change.action === 'UPDATE') {
|
||||||
|
actionBadgeClass = 'style="background-color: #ffc107; color: #000; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;"';
|
||||||
|
} else if (change.action === 'DELETE') {
|
||||||
|
actionBadgeClass = 'style="background-color: #dc3545; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;"';
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${change.attribute_code || 'N/A'}</td>
|
||||||
|
<td>${change.attribute_label || 'N/A'}</td>
|
||||||
|
<td>${change.store_id || 0}</td>
|
||||||
|
<td style="word-break: break-word;">${m1Value}</td>
|
||||||
|
<td style="word-break: break-word;">${m2Value}</td>
|
||||||
|
<td><span ${actionBadgeClass}>${actionText}</span></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td colspan="6" style="text-align: center; padding: 20px; color: #666;">
|
||||||
|
No changes in this table. Values are identical in M1 and M2.
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.appendChild(tbody);
|
||||||
|
tableWrapper.appendChild(table);
|
||||||
|
tablesContainer.appendChild(tableWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasAnyChanges) {
|
||||||
|
const noChangesMsg = document.createElement('div');
|
||||||
|
noChangesMsg.style.cssText = 'text-align: center; padding: 40px; color: #666; background: white; border: 1px solid #ddd; border-radius: 6px;';
|
||||||
|
noChangesMsg.textContent = 'No differences found. The product values are identical in M1 and M2 across all tables.';
|
||||||
|
tablesContainer.appendChild(noChangesMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const noDataMsg = document.createElement('div');
|
||||||
|
noDataMsg.style.cssText = 'text-align: center; padding: 40px; color: #666; background: white; border: 1px solid #ddd; border-radius: 6px;';
|
||||||
|
noDataMsg.textContent = 'No comparison data available.';
|
||||||
|
tablesContainer.appendChild(noDataMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Error case
|
||||||
|
comparisonMessage.textContent = data.message || 'Comparison failed';
|
||||||
|
comparisonMessage.style.background = '#f8d7da';
|
||||||
|
comparisonMessage.style.color = '#721c24';
|
||||||
|
comparisonMessage.style.border = '1px solid #f5c6cb';
|
||||||
|
|
||||||
|
const errorMsg = document.createElement('div');
|
||||||
|
errorMsg.style.cssText = 'text-align: center; padding: 40px; color: #dc3545; background: white; border: 1px solid #ddd; border-radius: 6px;';
|
||||||
|
errorMsg.textContent = data.message || 'Failed to compare product';
|
||||||
|
tablesContainer.appendChild(errorMsg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
compareBtn.disabled = false;
|
||||||
|
compareBtn.textContent = originalText;
|
||||||
|
comparisonSection.style.display = 'block';
|
||||||
|
|
||||||
|
const tablesContainer = document.getElementById('productComparisonTablesContainer');
|
||||||
|
if (tablesContainer) {
|
||||||
|
tablesContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
comparisonMessage.textContent = 'Error: ' + error.message;
|
||||||
|
comparisonMessage.style.background = '#f8d7da';
|
||||||
|
comparisonMessage.style.color = '#721c24';
|
||||||
|
comparisonMessage.style.border = '1px solid #f5c6cb';
|
||||||
|
|
||||||
|
if (tablesContainer) {
|
||||||
|
const errorMsg = document.createElement('div');
|
||||||
|
errorMsg.style.cssText = 'text-align: center; padding: 40px; color: #dc3545; background: white; border: 1px solid #ddd; border-radius: 6px;';
|
||||||
|
errorMsg.textContent = 'Error: ' + error.message;
|
||||||
|
tablesContainer.appendChild(errorMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Enter key support for SKU input
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const skuInput = document.getElementById('productSkuInput');
|
||||||
|
if (skuInput) {
|
||||||
|
skuInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
compareProductBySku();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Make functions available globally
|
// Make functions available globally
|
||||||
window.startProductMigration = startProductMigration;
|
window.startProductMigration = startProductMigration;
|
||||||
window.startProductOptionsMigration = startProductOptionsMigration;
|
window.startProductOptionsMigration = startProductOptionsMigration;
|
||||||
|
|
@ -555,4 +955,5 @@ window.syncProductCategories = syncProductCategories;
|
||||||
window.fixCategoryProducts = fixCategoryProducts;
|
window.fixCategoryProducts = fixCategoryProducts;
|
||||||
window.loadM1CategoryTreeWithProducts = loadM1CategoryTreeWithProducts;
|
window.loadM1CategoryTreeWithProducts = loadM1CategoryTreeWithProducts;
|
||||||
window.loadM2CategoryTreeWithProducts = loadM2CategoryTreeWithProducts;
|
window.loadM2CategoryTreeWithProducts = loadM2CategoryTreeWithProducts;
|
||||||
|
window.compareProductBySku = compareProductBySku;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,46 @@
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<!-- Product Migration Section -->
|
<!-- Migrate Single Product Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
<h2>🔍 Migrate Single Product</h2>
|
||||||
|
<div class="info-box" style="margin-bottom: 20px;">
|
||||||
|
<h3 style="margin-bottom: 10px; color: #1976D2; font-size: 1.1em;">Compare Product Values</h3>
|
||||||
|
<p style="margin: 5px 0; color: #1976D2;">Enter a product SKU to see all differences between Magento 1 and Magento 2 values:</p>
|
||||||
|
<ul style="margin: 10px 0 0 20px; color: #1976D2; line-height: 1.8;">
|
||||||
|
<li><strong>M1 Values:</strong> Values from Magento 1 that will be used to update Magento 2</li>
|
||||||
|
<li><strong>M2 Current Values:</strong> Current values in Magento 2</li>
|
||||||
|
<li><strong>Action:</strong> Shows whether the value will be INSERTED, UPDATED, or DELETED</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
|
||||||
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
|
<label for="productSkuInput" style="font-weight: 600; color: #333;">Product SKU:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="productSkuInput"
|
||||||
|
placeholder="Enter product SKU"
|
||||||
|
style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 1em; min-width: 200px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button id="compareProductBtn" class="btn btn-primary" onclick="compareProductBySku()">
|
||||||
|
Compare Product
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comparison Results -->
|
||||||
|
<div id="productComparisonSection" style="display: none; margin-top: 30px;">
|
||||||
|
<h3 style="margin-bottom: 15px; color: #333;">Comparison Results</h3>
|
||||||
|
<div id="productComparisonMessage" style="margin-bottom: 15px; padding: 10px; border-radius: 4px; background: #f0f0f0;"></div>
|
||||||
|
<div id="productComparisonTablesContainer">
|
||||||
|
<!-- Tables will be dynamically inserted here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Migration Section -->
|
||||||
|
<div class="section" style="margin-top: 30px;">
|
||||||
<h2>🚀 Product Migration</h2>
|
<h2>🚀 Product Migration</h2>
|
||||||
<div class="info-box" style="margin-bottom: 20px;">
|
<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 Product Migration"?</h3>
|
<h3 style="margin-bottom: 10px; color: #1976D2; font-size: 1.1em;">What happens when you click "Start Product Migration"?</h3>
|
||||||
|
|
@ -19,9 +57,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 20px; display: flex; gap: 15px; flex-wrap: wrap;">
|
<div style="margin-top: 20px; display: flex; gap: 15px; flex-wrap: wrap;">
|
||||||
<button id="dryRunProductMigrationBtn" class="btn btn-secondary" onclick="startProductMigration(true)">
|
|
||||||
Run Dry Run (Check for Errors)
|
|
||||||
</button>
|
|
||||||
<button id="startProductMigrationBtn" class="btn btn-primary" onclick="startProductMigration(false)">
|
<button id="startProductMigrationBtn" class="btn btn-primary" onclick="startProductMigration(false)">
|
||||||
Start Product Migration
|
Start Product Migration
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -29,16 +64,33 @@
|
||||||
Delete M2 Products Above M1 Max ID
|
Delete M2 Products Above M1 Max ID
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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="currentProduct">-</span> |
|
||||||
|
Added: <span id="progressAdded">0</span> |
|
||||||
|
Updated: <span id="progressUpdated">0</span> |
|
||||||
|
Errors: <span id="progressErrors">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Migration Logs Section -->
|
<!-- Migration Logs Section -->
|
||||||
<div class="section" style="margin-top: 30px;">
|
<div class="section" style="margin-top: 30px;">
|
||||||
<h2>📋 Product Migration Logs</h2>
|
<h2>📋 Product Migration Logs</h2>
|
||||||
<p style="margin-bottom: 15px; color: #666;">Detailed logs showing which products were added, updated, or encountered errors during migration:</p>
|
<p style="margin-bottom: 15px; color: #666;">Error logs showing which products encountered errors during migration:</p>
|
||||||
<div class="log-container" id="productMigrationLogContainer" style="display: block;">
|
<div class="log-container" id="productMigrationLogContainer" style="display: block;">
|
||||||
<div id="productMigrationLogContent">
|
<div id="productMigrationLogContent">
|
||||||
<div class="log-entry" style="color: #999; font-style: italic;">
|
<div class="log-entry" style="color: #999; font-style: italic;">
|
||||||
No product migration logs yet. Click "Run Dry Run" or "Start Product Migration" to begin.
|
No product migration logs yet. Click "Start Product Migration" to begin.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -281,11 +333,13 @@ class="btn btn-danger"
|
||||||
<script>
|
<script>
|
||||||
window.productRoutes = {
|
window.productRoutes = {
|
||||||
migrateProducts: '{{ route("products.migrate-products") }}',
|
migrateProducts: '{{ route("products.migrate-products") }}',
|
||||||
|
migrationProgress: '{{ route("products.migration-progress") }}',
|
||||||
migrateProductOptions: '{{ route("products.migrate-product-options") }}',
|
migrateProductOptions: '{{ route("products.migrate-product-options") }}',
|
||||||
deleteProduct: '{{ route("products.delete-product", ["productId" => ":id"]) }}',
|
deleteProduct: '{{ route("products.delete-product", ["productId" => ":id"]) }}',
|
||||||
deleteProductsAboveM1Max: '{{ route("products.delete-products-above-m1-max") }}',
|
deleteProductsAboveM1Max: '{{ route("products.delete-products-above-m1-max") }}',
|
||||||
syncProductCategories: '{{ route("products.sync-product-categories") }}',
|
syncProductCategories: '{{ route("products.sync-product-categories") }}',
|
||||||
fixCategoryProducts: '{{ route("products.fix-category-products") }}',
|
fixCategoryProducts: '{{ route("products.fix-category-products") }}',
|
||||||
|
compareProductBySku: '{{ route("products.compare-product-by-sku") }}',
|
||||||
magento1CategoryTreeWithProducts: '{{ route("categories.magento1-category-tree-with-products") }}',
|
magento1CategoryTreeWithProducts: '{{ route("categories.magento1-category-tree-with-products") }}',
|
||||||
magento2CategoryTreeWithProducts: '{{ route("categories.magento2-category-tree-with-products") }}'
|
magento2CategoryTreeWithProducts: '{{ route("categories.magento2-category-tree-with-products") }}'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,11 @@
|
||||||
Route::prefix('products')->name('products.')->group(function () {
|
Route::prefix('products')->name('products.')->group(function () {
|
||||||
Route::get('/', [ProductsController::class, 'index'])->name('index');
|
Route::get('/', [ProductsController::class, 'index'])->name('index');
|
||||||
Route::post('/migrate', [ProductsController::class, 'migrateProducts'])->name('migrate-products');
|
Route::post('/migrate', [ProductsController::class, 'migrateProducts'])->name('migrate-products');
|
||||||
|
Route::get('/migration-progress', [ProductsController::class, 'getMigrationProgress'])->name('migration-progress');
|
||||||
Route::post('/migrate-options', [ProductsController::class, 'migrateProductOptions'])->name('migrate-product-options');
|
Route::post('/migrate-options', [ProductsController::class, 'migrateProductOptions'])->name('migrate-product-options');
|
||||||
Route::post('/sync-categories', [ProductsController::class, 'syncProductCategories'])->name('sync-product-categories');
|
Route::post('/sync-categories', [ProductsController::class, 'syncProductCategories'])->name('sync-product-categories');
|
||||||
Route::post('/fix-category-products', [ProductsController::class, 'fixCategoryProducts'])->name('fix-category-products');
|
Route::post('/fix-category-products', [ProductsController::class, 'fixCategoryProducts'])->name('fix-category-products');
|
||||||
|
Route::post('/compare-by-sku', [ProductsController::class, 'compareProductBySku'])->name('compare-product-by-sku');
|
||||||
Route::delete('/{productId}', [ProductsController::class, 'deleteM2Product'])->name('delete-product');
|
Route::delete('/{productId}', [ProductsController::class, 'deleteM2Product'])->name('delete-product');
|
||||||
Route::delete('/above-m1-max', [ProductsController::class, 'deleteM2ProductsAboveM1Max'])->name('delete-products-above-m1-max');
|
Route::delete('/above-m1-max', [ProductsController::class, 'deleteM2ProductsAboveM1Max'])->name('delete-products-above-m1-max');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue