broke out code
This commit is contained in:
parent
54ffc41d46
commit
99474ccd75
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
use tailwind standards for css
|
||||
use blade partials for reusable components
|
||||
tabs should be specific pages with specific controllers
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\MagentoCategoryMigrationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AttributesController extends Controller
|
||||
{
|
||||
protected $migrationService;
|
||||
|
||||
public function __construct(MagentoCategoryMigrationService $migrationService)
|
||||
{
|
||||
$this->migrationService = $migrationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the attributes page
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$m1Attributes = $this->migrationService->getMagento1Attributes();
|
||||
$m2Attributes = $this->migrationService->getMagento2Attributes();
|
||||
$m1AttributesMissingInM2 = $this->migrationService->getM1AttributesMissingInM2();
|
||||
$m1AttributeGroups = $this->migrationService->getMagento1AttributeGroups();
|
||||
$m2AttributeGroups = $this->migrationService->getMagento2AttributeGroups();
|
||||
$m1AttributeGroupsMissingInM2 = $this->migrationService->getM1AttributeGroupsMissingInM2();
|
||||
|
||||
return view('attributes.index', [
|
||||
'm1Attributes' => $m1Attributes,
|
||||
'm2Attributes' => $m2Attributes,
|
||||
'm1AttributesMissingInM2' => $m1AttributesMissingInM2,
|
||||
'm1AttributeGroups' => $m1AttributeGroups,
|
||||
'm2AttributeGroups' => $m2AttributeGroups,
|
||||
'm1AttributeGroupsMissingInM2' => $m1AttributeGroupsMissingInM2,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate an attribute from Magento 1 to Magento 2
|
||||
*/
|
||||
public function migrateAttribute(Request $request, $attributeId)
|
||||
{
|
||||
try {
|
||||
$result = $this->migrationService->migrateAttribute($attributeId);
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Attribute migration error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Migration failed: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate an attribute group from Magento 1 to Magento 2
|
||||
*/
|
||||
public function migrateAttributeGroup(Request $request, $groupId, $setId)
|
||||
{
|
||||
try {
|
||||
$result = $this->migrationService->migrateAttributeGroup($groupId, $setId);
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Attribute group migration error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Migration failed: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\MagentoCategoryMigrationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CategoriesController extends Controller
|
||||
{
|
||||
protected $migrationService;
|
||||
|
||||
public function __construct(MagentoCategoryMigrationService $migrationService)
|
||||
{
|
||||
$this->migrationService = $migrationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the categories page
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$m1Categories = $this->migrationService->getMagento1Categories();
|
||||
$m2Categories = $this->migrationService->getMagento2Categories();
|
||||
$m2CategoriesNotInM1 = $this->migrationService->getM2CategoriesNotInM1();
|
||||
|
||||
return view('categories.index', [
|
||||
'm1CategoriesCount' => $m1Categories->count(),
|
||||
'm2CategoriesCount' => $m2Categories->count(),
|
||||
'm2CategoriesNotInM1' => $m2CategoriesNotInM1,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Magento 1 categories preview
|
||||
*/
|
||||
public function getMagento1Categories()
|
||||
{
|
||||
$categories = $this->migrationService->getMagento1Categories();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'count' => $categories->count(),
|
||||
'categories' => $categories->take(50)->map(function($cat) {
|
||||
return [
|
||||
'id' => $cat->entity_id,
|
||||
'name' => $cat->name ?? 'N/A',
|
||||
'level' => $cat->level,
|
||||
'parent_id' => $cat->parent_id,
|
||||
'is_active' => $cat->is_active ?? 0,
|
||||
];
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Magento 1 category tree
|
||||
*/
|
||||
public function getMagento1CategoryTree()
|
||||
{
|
||||
try {
|
||||
$categories = $this->migrationService->getMagento1Categories();
|
||||
$tree = $this->migrationService->buildCategoryTreeHierarchy($categories, 'm1');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'tree' => $tree,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching M1 category tree: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch category tree: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Magento 1 category tree with products
|
||||
*/
|
||||
public function getMagento1CategoryTreeWithProducts()
|
||||
{
|
||||
try {
|
||||
$tree = $this->migrationService->getMagento1CategoryTreeWithProducts();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'tree' => $tree
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching M1 category tree with products: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to load category tree with products: ' . $e->getMessage(),
|
||||
'tree' => []
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Magento 2 category tree
|
||||
*/
|
||||
public function getMagento2CategoryTree()
|
||||
{
|
||||
try {
|
||||
$categories = $this->migrationService->getMagento2Categories();
|
||||
$tree = $this->migrationService->buildCategoryTreeHierarchy($categories, 'm2');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'tree' => $tree,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching M2 category tree: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch category tree: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Magento 2 category tree with products
|
||||
*/
|
||||
public function getMagento2CategoryTreeWithProducts()
|
||||
{
|
||||
try {
|
||||
$tree = $this->migrationService->getMagento2CategoryTreeWithProducts();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'tree' => $tree
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching M2 category tree with products: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to load category tree with products: ' . $e->getMessage(),
|
||||
'tree' => []
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get M2 categories not in M1 (for AJAX refresh)
|
||||
*/
|
||||
public function getM2CategoriesNotInM1()
|
||||
{
|
||||
try {
|
||||
$categories = $this->migrationService->getM2CategoriesNotInM1();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'categories' => $categories->map(function($category) {
|
||||
return [
|
||||
'entity_id' => $category->entity_id,
|
||||
'name' => $category->name ?? 'Unnamed Category',
|
||||
'level' => $category->level ?? 'N/A',
|
||||
'is_active' => $category->is_active ?? 0,
|
||||
'root_category_name' => $category->root_category_name ?? 'N/A',
|
||||
'root_category_id' => $category->root_category_id ?? null,
|
||||
'path' => $category->path ?? 'N/A',
|
||||
];
|
||||
}),
|
||||
'count' => $categories->count(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching M2 categories not in M1: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch categories: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a category
|
||||
*/
|
||||
public function deleteCategory(Request $request, $categoryId)
|
||||
{
|
||||
$request->validate([
|
||||
'source' => 'nullable|in:m1,m2',
|
||||
]);
|
||||
|
||||
try {
|
||||
$source = $request->input('source', 'm2');
|
||||
$result = $this->migrationService->deleteCategory($categoryId, $source);
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Category deletion error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Deletion failed: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a Magento 2 category
|
||||
*/
|
||||
public function renameCategory(Request $request, $categoryId)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'store_id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
try {
|
||||
$newName = $request->input('name');
|
||||
$storeId = $request->input('store_id', 0);
|
||||
$result = $this->migrationService->renameCategory($categoryId, $newName, $storeId);
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Category rename error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Rename failed: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\MagentoCategoryMigrationService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ConnectionsController extends Controller
|
||||
{
|
||||
protected $migrationService;
|
||||
|
||||
public function __construct(MagentoCategoryMigrationService $migrationService)
|
||||
{
|
||||
$this->migrationService = $migrationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the database connections page
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$connectionTest = $this->migrationService->testConnections();
|
||||
|
||||
return view('connections.index', [
|
||||
'connectionTest' => $connectionTest,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test database connections
|
||||
*/
|
||||
public function testConnections()
|
||||
{
|
||||
$results = $this->migrationService->testConnections();
|
||||
|
||||
return response()->json([
|
||||
'success' => $results['magento1'] && $results['magento2'],
|
||||
'results' => $results,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\MagentoCategoryMigrationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MigrationController extends Controller
|
||||
{
|
||||
protected $migrationService;
|
||||
|
||||
public function __construct(MagentoCategoryMigrationService $migrationService)
|
||||
{
|
||||
$this->migrationService = $migrationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the category migration page
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$m1Stores = $this->migrationService->getMagento1Stores();
|
||||
$m2Stores = $this->migrationService->getMagento2Stores();
|
||||
$connectionTest = $this->migrationService->testConnections();
|
||||
$m1Categories = $this->migrationService->getMagento1Categories();
|
||||
$m2Categories = $this->migrationService->getMagento2Categories();
|
||||
|
||||
return view('migration.index', [
|
||||
'm1Stores' => $m1Stores,
|
||||
'm2Stores' => $m2Stores,
|
||||
'connectionTest' => $connectionTest,
|
||||
'm1CategoriesCount' => $m1Categories->count(),
|
||||
'm2CategoriesCount' => $m2Categories->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the migration
|
||||
*/
|
||||
public function migrate(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'store_mapping' => 'required|array',
|
||||
'store_mapping.*' => 'required|integer',
|
||||
]);
|
||||
|
||||
try {
|
||||
$storeMapping = $request->input('store_mapping');
|
||||
|
||||
// Convert array format: ['m1_store_id' => 'm2_store_id']
|
||||
$mapping = [];
|
||||
foreach ($storeMapping as $m1StoreId => $m2StoreId) {
|
||||
$mapping[(int)$m1StoreId] = (int)$m2StoreId;
|
||||
}
|
||||
|
||||
$result = $this->migrationService->migrateCategories($mapping);
|
||||
|
||||
return response()->json($result);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Migration error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Migration failed: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\MagentoCategoryMigrationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProductsController extends Controller
|
||||
{
|
||||
protected $migrationService;
|
||||
|
||||
public function __construct(MagentoCategoryMigrationService $migrationService)
|
||||
{
|
||||
$this->migrationService = $migrationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the products page
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$m1Products = $this->migrationService->getMagento1Products();
|
||||
$m2Products = $this->migrationService->getMagento2Products();
|
||||
$m1ProductsNotInM2 = $this->migrationService->getM1ProductsNotInM2();
|
||||
$m2ProductsNotInM1 = $this->migrationService->getM2ProductsNotInM1();
|
||||
|
||||
return view('products.index', [
|
||||
'm1Products' => $m1Products,
|
||||
'm2Products' => $m2Products,
|
||||
'm1ProductsNotInM2' => $m1ProductsNotInM2,
|
||||
'm2ProductsNotInM1' => $m2ProductsNotInM1,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all products from Magento 1 to Magento 2
|
||||
*/
|
||||
public function migrateProducts(Request $request)
|
||||
{
|
||||
try {
|
||||
$dryRun = $request->input('dry_run', false);
|
||||
$result = $this->migrationService->migrateProducts($dryRun);
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Product migration error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Migration failed: ' . $e->getMessage(),
|
||||
'added' => 0,
|
||||
'updated' => 0,
|
||||
'errors' => 0,
|
||||
'log' => [],
|
||||
'missing_attributes' => []
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single product from Magento 2
|
||||
*/
|
||||
public function deleteM2Product(Request $request, $productId)
|
||||
{
|
||||
try {
|
||||
$result = $this->migrationService->deleteM2Product($productId);
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Delete M2 product error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Deletion failed: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync product category assignments from M1 to M2
|
||||
*/
|
||||
public function syncProductCategories(Request $request)
|
||||
{
|
||||
try {
|
||||
$result = $this->migrationService->syncProductCategories();
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Sync product categories error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Sync failed: ' . $e->getMessage(),
|
||||
'updated' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => 0,
|
||||
'log' => []
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all products in Magento 2 that have entity_id greater than max M1 product ID
|
||||
*/
|
||||
public function deleteM2ProductsAboveM1Max(Request $request)
|
||||
{
|
||||
try {
|
||||
$result = $this->migrationService->deleteM2ProductsAboveM1Max();
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Delete M2 products error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Deletion failed: ' . $e->getMessage(),
|
||||
'deleted' => 0
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,10 @@
|
|||
@import 'tailwindcss';
|
||||
@import './base.css';
|
||||
@import './connections.css';
|
||||
@import './categories.css';
|
||||
@import './migration.css';
|
||||
@import './attributes.css';
|
||||
@import './products.css';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/* Attributes page specific styles */
|
||||
.attributes-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.attributes-table thead tr {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.attributes-table th {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.attributes-table td {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.85em;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.attributes-table tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 90%;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #b71c1c;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #2196F3;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 5px 0;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-card .number {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
margin-top: 15px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navigation {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #ddd;
|
||||
margin-bottom: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 15px 30px;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: #e9ecef;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
background: white;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
/* Categories page specific styles */
|
||||
|
||||
/* Tree View */
|
||||
.tree-container {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.tree-node-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tree-node-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.tree-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-toggle.expanded::before {
|
||||
content: '▼';
|
||||
}
|
||||
|
||||
.tree-toggle.collapsed::before {
|
||||
content: '▶';
|
||||
}
|
||||
|
||||
.tree-toggle.leaf {
|
||||
width: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tree-label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tree-label-text {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tree-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tree-badge.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.tree-badge.inactive {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.tree-children {
|
||||
margin-left: 28px;
|
||||
border-left: 2px solid #e0e0e0;
|
||||
padding-left: 12px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-children.expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tree-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tree-delete-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75em;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tree-delete-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.tree-delete-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tree-rename-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75em;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tree-rename-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.tree-rename-input {
|
||||
padding: 4px 8px;
|
||||
border: 2px solid #28a745;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
width: 200px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tree-rename-input:focus {
|
||||
outline: none;
|
||||
border-color: #218838;
|
||||
}
|
||||
|
||||
.tree-rename-actions {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tree-rename-save-btn, .tree-rename-cancel-btn {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tree-rename-save-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tree-rename-save-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.tree-rename-cancel-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tree-rename-cancel-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
/* Delete Confirmation Popup */
|
||||
.delete-confirm-popup {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
padding: 20px;
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
z-index: 10000;
|
||||
border: 1px solid #e0e0e0;
|
||||
animation: popupFadeIn 0.2s ease-out;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.delete-confirm-popup.no-arrow::before,
|
||||
.delete-confirm-popup.no-arrow::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes popupFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.delete-confirm-popup::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid white;
|
||||
}
|
||||
|
||||
.delete-confirm-popup::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -9px;
|
||||
left: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.delete-confirm-popup h3 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #d32f2f;
|
||||
font-size: 1.1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.delete-confirm-popup h3::before {
|
||||
content: '⚠️';
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.delete-confirm-popup p {
|
||||
margin: 0 0 16px 0;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.delete-confirm-popup .popup-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.delete-confirm-popup .popup-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.delete-confirm-popup .popup-btn-cancel {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.delete-confirm-popup .popup-btn-cancel:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.delete-confirm-popup .popup-btn-delete {
|
||||
background: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-confirm-popup .popup-btn-delete:hover {
|
||||
background: #b71c1c;
|
||||
}
|
||||
|
||||
.popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/* Connections page specific styles */
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/* Migration page specific styles */
|
||||
|
||||
.store-mapping {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.store-select {
|
||||
padding: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.store-select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 1.5em;
|
||||
color: #667eea;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/* Products page specific styles */
|
||||
.products-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.products-table thead tr {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.products-table th {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.products-table td {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.85em;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.products-table tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
// Attributes page JavaScript
|
||||
|
||||
let routes = {};
|
||||
let csrfToken = '';
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.attributeRoutes) {
|
||||
routes = window.attributeRoutes;
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
});
|
||||
|
||||
function migrateAttribute(attributeId, attributeCode) {
|
||||
if (!confirm(`Are you sure you want to migrate the attribute "${attributeCode}" from Magento 1 to Magento 2?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.getElementById(`migrate-btn-${attributeId}`);
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Migrating...';
|
||||
|
||||
const url = routes.migrateAttribute.replace(':id', attributeId);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const row = document.getElementById(`attr-row-${attributeId}`);
|
||||
if (row) {
|
||||
row.style.opacity = '0.5';
|
||||
row.style.background = '#d4edda';
|
||||
button.textContent = 'Migrated ✓';
|
||||
button.style.background = '#28a745';
|
||||
button.disabled = true;
|
||||
|
||||
const totalSpan = document.getElementById('missingAttributesTotal');
|
||||
if (totalSpan) {
|
||||
const currentCount = parseInt(totalSpan.textContent);
|
||||
if (currentCount > 0) {
|
||||
totalSpan.textContent = currentCount - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
alert('Attribute migrated successfully!');
|
||||
} else {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
alert('Error: ' + (data.message || 'Failed to migrate attribute'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function migrateAttributeGroup(groupId, setId, groupName) {
|
||||
if (!confirm(`Are you sure you want to migrate the attribute group "${groupName}" from Magento 1 to Magento 2?\n\nThis will also migrate any attributes in this group that exist in Magento 2.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.getElementById(`migrate-group-btn-${groupId}-${setId}`);
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Migrating...';
|
||||
|
||||
const url = routes.migrateAttributeGroup
|
||||
.replace(':groupId', groupId)
|
||||
.replace(':setId', setId);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const row = document.getElementById(`group-row-${groupId}-${setId}`);
|
||||
if (row) {
|
||||
row.style.opacity = '0.5';
|
||||
row.style.background = '#d4edda';
|
||||
button.textContent = 'Migrated ✓';
|
||||
button.style.background = '#28a745';
|
||||
button.disabled = true;
|
||||
}
|
||||
const message = data.attributes_migrated !== undefined
|
||||
? `Attribute group migrated successfully! ${data.attributes_migrated} attribute(s) were also migrated to the group.`
|
||||
: 'Attribute group migrated successfully!';
|
||||
alert(message);
|
||||
} else {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
alert('Error: ' + (data.message || 'Failed to migrate attribute group'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Make functions available globally
|
||||
window.migrateAttribute = migrateAttribute;
|
||||
window.migrateAttributeGroup = migrateAttributeGroup;
|
||||
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
// Categories page JavaScript
|
||||
|
||||
let routes = {};
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.categoryRoutes) {
|
||||
routes = window.categoryRoutes;
|
||||
loadCategoryTrees();
|
||||
}
|
||||
});
|
||||
|
||||
function loadCategoryTrees() {
|
||||
loadM1Tree();
|
||||
loadM2Tree();
|
||||
}
|
||||
|
||||
function loadM1Tree() {
|
||||
const container = document.getElementById('m1-tree-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="tree-loading">Loading categories...</div>';
|
||||
|
||||
fetch(routes.magento1CategoryTree)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
container.innerHTML = '';
|
||||
if (data.tree && data.tree.length > 0) {
|
||||
renderTree(container, data.tree, 'm1');
|
||||
} else {
|
||||
container.innerHTML = '<div class="tree-loading">No categories found</div>';
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${data.message || 'Failed to load categories'}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function loadM2Tree() {
|
||||
const container = document.getElementById('m2-tree-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="tree-loading">Loading categories...</div>';
|
||||
|
||||
fetch(routes.magento2CategoryTree)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
container.innerHTML = '';
|
||||
if (data.tree && data.tree.length > 0) {
|
||||
renderTree(container, data.tree, 'm2');
|
||||
} else {
|
||||
container.innerHTML = '<div class="tree-loading">No categories found</div>';
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${data.message || 'Failed to load categories'}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderTree(container, nodes, source = 'm2') {
|
||||
nodes.forEach(node => {
|
||||
const nodeElement = createTreeNode(node, source);
|
||||
container.appendChild(nodeElement);
|
||||
});
|
||||
}
|
||||
|
||||
function createTreeNode(node, source = 'm2') {
|
||||
const nodeDiv = document.createElement('div');
|
||||
nodeDiv.className = 'tree-node';
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const canDelete = node.id != 0 && node.id != 1;
|
||||
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.className = 'tree-node-item';
|
||||
|
||||
const toggle = document.createElement('span');
|
||||
toggle.className = hasChildren ? 'tree-toggle collapsed' : 'tree-toggle leaf';
|
||||
if (hasChildren) {
|
||||
toggle.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
toggleNode(this);
|
||||
};
|
||||
}
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'tree-label';
|
||||
|
||||
const labelText = document.createElement('span');
|
||||
labelText.className = 'tree-label-text';
|
||||
labelText.textContent = `[${node.id}] ${node.name || 'Unnamed Category'}`;
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `tree-badge ${node.is_active ? 'active' : 'inactive'}`;
|
||||
badge.textContent = node.is_active ? 'Active' : 'Inactive';
|
||||
|
||||
label.appendChild(labelText);
|
||||
label.appendChild(badge);
|
||||
|
||||
if (canDelete) {
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'tree-delete-btn';
|
||||
deleteBtn.textContent = 'Delete';
|
||||
deleteBtn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
showDeleteConfirmPopup(deleteBtn, node.id, node.name, source, hasChildren, node.children ? node.children.length : 0);
|
||||
};
|
||||
label.appendChild(deleteBtn);
|
||||
}
|
||||
|
||||
itemDiv.appendChild(toggle);
|
||||
itemDiv.appendChild(label);
|
||||
nodeDiv.appendChild(itemDiv);
|
||||
|
||||
if (hasChildren) {
|
||||
const childrenDiv = document.createElement('div');
|
||||
childrenDiv.className = 'tree-children';
|
||||
node.children.forEach(child => {
|
||||
childrenDiv.appendChild(createTreeNode(child, source));
|
||||
});
|
||||
nodeDiv.appendChild(childrenDiv);
|
||||
}
|
||||
|
||||
return nodeDiv;
|
||||
}
|
||||
|
||||
function toggleNode(toggleElement) {
|
||||
const nodeItem = toggleElement.parentElement;
|
||||
const nodeDiv = nodeItem.parentElement;
|
||||
const childrenDivs = nodeDiv.querySelectorAll('.tree-children');
|
||||
|
||||
if (childrenDivs.length > 0) {
|
||||
let isExpanded = false;
|
||||
childrenDivs.forEach(div => {
|
||||
if (div.style.display !== 'none') {
|
||||
isExpanded = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isExpanded) {
|
||||
childrenDivs.forEach(div => {
|
||||
div.style.display = 'none';
|
||||
});
|
||||
toggleElement.classList.remove('expanded');
|
||||
toggleElement.classList.add('collapsed');
|
||||
} else {
|
||||
childrenDivs.forEach(div => {
|
||||
div.style.display = 'block';
|
||||
});
|
||||
toggleElement.classList.remove('collapsed');
|
||||
toggleElement.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteConfirmPopup(buttonElement, categoryId, categoryName, source, hasChildren, childrenCount) {
|
||||
const existingPopup = document.querySelector('.delete-confirm-popup');
|
||||
const existingOverlay = document.querySelector('.popup-overlay');
|
||||
if (existingPopup) existingPopup.remove();
|
||||
if (existingOverlay) existingOverlay.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'popup-overlay';
|
||||
overlay.onclick = function() {
|
||||
closeDeleteConfirmPopup();
|
||||
};
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'delete-confirm-popup';
|
||||
popup.style.position = 'fixed';
|
||||
popup.style.top = '50%';
|
||||
popup.style.left = '50%';
|
||||
popup.style.transform = 'translate(-50%, -50%)';
|
||||
popup.style.zIndex = '10000';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Delete Category';
|
||||
|
||||
const message = document.createElement('p');
|
||||
if (hasChildren) {
|
||||
message.innerHTML = `Are you sure you want to delete <strong style="color: #d32f2f; font-weight: bold;">"${categoryName}"</strong> and all ${childrenCount} subcategory(ies)? This action cannot be undone.`;
|
||||
} else {
|
||||
message.innerHTML = `Are you sure you want to delete the category <strong style="color: #d32f2f; font-weight: bold;">"${categoryName}"</strong>? This action cannot be undone.`;
|
||||
}
|
||||
|
||||
const buttonsDiv = document.createElement('div');
|
||||
buttonsDiv.className = 'popup-buttons';
|
||||
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'popup-btn popup-btn-cancel';
|
||||
cancelBtn.textContent = 'Cancel';
|
||||
cancelBtn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
closeDeleteConfirmPopup();
|
||||
};
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'popup-btn popup-btn-delete';
|
||||
deleteBtn.textContent = 'Delete';
|
||||
deleteBtn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
closeDeleteConfirmPopup();
|
||||
deleteCategory(categoryId, categoryName, source);
|
||||
};
|
||||
|
||||
buttonsDiv.appendChild(cancelBtn);
|
||||
buttonsDiv.appendChild(deleteBtn);
|
||||
|
||||
popup.appendChild(title);
|
||||
popup.appendChild(message);
|
||||
popup.appendChild(buttonsDiv);
|
||||
|
||||
document.body.appendChild(popup);
|
||||
}
|
||||
|
||||
function closeDeleteConfirmPopup() {
|
||||
const popup = document.querySelector('.delete-confirm-popup');
|
||||
const overlay = document.querySelector('.popup-overlay');
|
||||
if (popup) popup.remove();
|
||||
if (overlay) overlay.remove();
|
||||
}
|
||||
|
||||
function deleteCategory(categoryId, categoryName, source) {
|
||||
const container = source === 'm1' ? document.getElementById('m1-tree-container') : document.getElementById('m2-tree-container');
|
||||
if (!container) return;
|
||||
|
||||
const originalContent = container.innerHTML;
|
||||
container.innerHTML = '<div class="tree-loading">Deleting category...</div>';
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
const deleteRoute = routes.deleteCategory.replace(':id', categoryId);
|
||||
|
||||
fetch(deleteRoute, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ source: source })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (source === 'm1') {
|
||||
loadM1Tree();
|
||||
} else {
|
||||
loadM2Tree();
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = originalContent;
|
||||
alert('Error: ' + (data.message || 'Failed to delete category'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = originalContent;
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Connections page JavaScript
|
||||
export function initConnections() {
|
||||
const testRoute = document.querySelector('[data-test-route]')?.dataset.testRoute;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
|
||||
window.testConnections = function() {
|
||||
fetch(testRoute, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('✓ Both database connections are working!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('✗ Connection test failed. Check the error messages above.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error testing connections: ' + error.message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
// Migration page JavaScript
|
||||
|
||||
let routes = {};
|
||||
let csrfToken = '';
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.migrationRoutes) {
|
||||
routes = window.migrationRoutes;
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
});
|
||||
|
||||
function startMigration() {
|
||||
const storeMapping = {};
|
||||
const selects = document.querySelectorAll('select[name^="store_mapping"]');
|
||||
|
||||
let hasMapping = false;
|
||||
selects.forEach(select => {
|
||||
if (select.value) {
|
||||
storeMapping[select.name.match(/\[(\d+)\]/)[1]] = select.value;
|
||||
hasMapping = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasMapping) {
|
||||
alert('Please map at least one store before starting migration.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Are you sure you want to start the migration? This will modify your Magento 2 database.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('migrateBtn').disabled = true;
|
||||
document.getElementById('loading').classList.add('active');
|
||||
|
||||
const migrationLogContent = document.getElementById('migrationLogContent');
|
||||
migrationLogContent.innerHTML = '<div class="log-entry">Starting migration...</div>';
|
||||
|
||||
document.getElementById('migrationLogContainer').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
||||
fetch(routes.migrate, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ store_mapping: storeMapping })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('loading').classList.remove('active');
|
||||
document.getElementById('migrateBtn').disabled = false;
|
||||
|
||||
if (data.success) {
|
||||
addLogEntry(`✓ Migration completed successfully!`, 'success');
|
||||
addLogEntry(`Total categories processed: ${data.migrated_count}`, 'success');
|
||||
if (data.added_count !== undefined) {
|
||||
addLogEntry(` - Added new categories: ${data.added_count}`, 'success');
|
||||
}
|
||||
if (data.existing_count !== undefined) {
|
||||
addLogEntry(` - Found existing categories: ${data.existing_count}`, 'success');
|
||||
}
|
||||
|
||||
if (data.log && data.log.length > 0) {
|
||||
data.log.forEach(log => {
|
||||
const type = log.includes('ERROR') ? 'error' : 'success';
|
||||
addLogEntry(log, type);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addLogEntry(`✗ Migration failed: ${data.message}`, 'error');
|
||||
if (data.log && data.log.length > 0) {
|
||||
data.log.forEach(log => {
|
||||
addLogEntry(log, log.includes('ERROR') ? 'error' : 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('loading').classList.remove('active');
|
||||
document.getElementById('migrateBtn').disabled = false;
|
||||
addLogEntry(`✗ Error: ${error.message}`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function addLogEntry(message, type = '') {
|
||||
const migrationLogContent = document.getElementById('migrationLogContent');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry ${type}`;
|
||||
entry.textContent = message;
|
||||
migrationLogContent.appendChild(entry);
|
||||
|
||||
const migrationLogContainer = document.getElementById('migrationLogContainer');
|
||||
migrationLogContainer.scrollTop = migrationLogContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// Make startMigration available globally
|
||||
window.startMigration = startMigration;
|
||||
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
// Products page JavaScript
|
||||
|
||||
let routes = {};
|
||||
let csrfToken = '';
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.productRoutes) {
|
||||
routes = window.productRoutes;
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
});
|
||||
|
||||
function startProductMigration(dryRun) {
|
||||
const button = dryRun ? document.getElementById('dryRunProductMigrationBtn') : document.getElementById('startProductMigrationBtn');
|
||||
const otherButton = dryRun ? document.getElementById('startProductMigrationBtn') : document.getElementById('dryRunProductMigrationBtn');
|
||||
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('productMigrationLogContent');
|
||||
logContent.innerHTML = '<div class="log-entry">' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '</div>';
|
||||
|
||||
fetch(routes.migrateProducts, {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
if (data.missing_attributes && data.missing_attributes.length > 0) {
|
||||
const missingSection = document.getElementById('missingAttributesSection');
|
||||
const tbody = document.getElementById('missingAttributesTableBody');
|
||||
tbody.innerHTML = '';
|
||||
data.missing_attributes.forEach(attr => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${attr.attribute_code}</td>
|
||||
<td>${attr.frontend_label || 'N/A'}</td>
|
||||
<td>${attr.backend_type || 'N/A'}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
missingSection.style.display = 'block';
|
||||
}
|
||||
|
||||
const logContainer = document.getElementById('productMigrationLogContainer');
|
||||
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 deleteM2Product(productId, productName, productSku) {
|
||||
if (!confirm(`Are you sure you want to delete product "${productName}" (SKU: ${productSku})?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = routes.deleteProduct.replace(':id', productId);
|
||||
|
||||
fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Product deleted successfully!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Failed to delete product'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteProductsAboveM1Max() {
|
||||
if (!confirm('Are you sure you want to delete all Magento 2 products with entity_id greater than the maximum Magento 1 product ID? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.getElementById('deleteProductsAboveM1MaxBtn');
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Deleting...';
|
||||
|
||||
fetch(routes.deleteProductsAboveM1Max, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
|
||||
if (data.success) {
|
||||
alert(`Successfully deleted ${data.deleted || 0} products.`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Failed to delete products'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function syncProductCategories() {
|
||||
if (!confirm('Are you sure you want to sync product category assignments from Magento 1 to Magento 2?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.getElementById('syncProductCategoriesBtn');
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Syncing...';
|
||||
|
||||
fetch(routes.syncProductCategories, {
|
||||
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) {
|
||||
alert(`Sync completed! Updated: ${data.updated || 0}, Skipped: ${data.skipped || 0}, Errors: ${data.errors || 0}`);
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Sync failed'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function loadM1CategoryTreeWithProducts() {
|
||||
const container = document.getElementById('m1-category-products-tree-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="tree-loading">Loading categories and products...</div>';
|
||||
|
||||
fetch(routes.magento1CategoryTreeWithProducts)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
container.innerHTML = '';
|
||||
if (data.tree && data.tree.length > 0) {
|
||||
renderCategoryTreeWithProducts(container, data.tree);
|
||||
} else {
|
||||
container.innerHTML = '<div class="tree-loading">No categories found</div>';
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${data.message || 'Failed to load categories'}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function loadM2CategoryTreeWithProducts() {
|
||||
const container = document.getElementById('m2-category-products-tree-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="tree-loading">Loading categories and products...</div>';
|
||||
|
||||
fetch(routes.magento2CategoryTreeWithProducts)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
container.innerHTML = '';
|
||||
if (data.tree && data.tree.length > 0) {
|
||||
renderCategoryTreeWithProducts(container, data.tree);
|
||||
} else {
|
||||
container.innerHTML = '<div class="tree-loading">No categories found</div>';
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${data.message || 'Failed to load categories'}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="tree-error">Error: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderCategoryTreeWithProducts(container, tree) {
|
||||
tree.forEach(node => {
|
||||
const nodeElement = createCategoryWithProductsNode(node);
|
||||
container.appendChild(nodeElement);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleNode(toggleElement) {
|
||||
const nodeItem = toggleElement.parentElement;
|
||||
const nodeDiv = nodeItem.parentElement;
|
||||
const childrenDivs = nodeDiv.querySelectorAll('.tree-children');
|
||||
|
||||
if (childrenDivs.length > 0) {
|
||||
let isExpanded = false;
|
||||
childrenDivs.forEach(div => {
|
||||
if (div.style.display !== 'none') {
|
||||
isExpanded = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isExpanded) {
|
||||
childrenDivs.forEach(div => {
|
||||
div.style.display = 'none';
|
||||
});
|
||||
toggleElement.classList.remove('expanded');
|
||||
toggleElement.classList.add('collapsed');
|
||||
} else {
|
||||
childrenDivs.forEach(div => {
|
||||
div.style.display = 'block';
|
||||
});
|
||||
toggleElement.classList.remove('collapsed');
|
||||
toggleElement.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createCategoryWithProductsNode(node) {
|
||||
const nodeDiv = document.createElement('div');
|
||||
nodeDiv.className = 'tree-node';
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const hasProducts = node.products && node.products.length > 0;
|
||||
const productCount = node.product_count !== undefined ? node.product_count : (node.products ? node.products.length : 0);
|
||||
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.className = 'tree-node-item';
|
||||
|
||||
const toggle = document.createElement('span');
|
||||
toggle.className = (hasChildren || hasProducts) ? 'tree-toggle collapsed' : 'tree-toggle leaf';
|
||||
if (hasChildren || hasProducts) {
|
||||
toggle.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
toggleNode(this);
|
||||
};
|
||||
}
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'tree-label';
|
||||
|
||||
const labelText = document.createElement('span');
|
||||
labelText.className = 'tree-label-text';
|
||||
labelText.textContent = `[${node.id}] ${node.name || 'Unnamed Category'} (${productCount} products)`;
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `tree-badge ${node.is_active ? 'active' : 'inactive'}`;
|
||||
badge.textContent = node.is_active ? 'Active' : 'Inactive';
|
||||
|
||||
label.appendChild(labelText);
|
||||
label.appendChild(badge);
|
||||
|
||||
itemDiv.appendChild(toggle);
|
||||
itemDiv.appendChild(label);
|
||||
nodeDiv.appendChild(itemDiv);
|
||||
|
||||
// Add products section if products exist
|
||||
if (hasProducts) {
|
||||
const productsDiv = document.createElement('div');
|
||||
productsDiv.className = 'tree-children';
|
||||
productsDiv.style.display = 'none';
|
||||
|
||||
const productsHeader = document.createElement('div');
|
||||
productsHeader.style.cssText = 'padding: 8px 12px; font-weight: 600; color: #667eea; background: #f0f0f0; border-radius: 4px; margin: 5px 0;';
|
||||
productsHeader.textContent = `Products (${node.products.length}):`;
|
||||
productsDiv.appendChild(productsHeader);
|
||||
|
||||
node.products.forEach(product => {
|
||||
const productDiv = document.createElement('div');
|
||||
productDiv.style.cssText = 'padding: 6px 12px 6px 30px; font-size: 0.9em; color: #666; border-left: 2px solid #e0e0e0; margin-left: 20px;';
|
||||
productDiv.textContent = `ID: ${product.id || product.product_id || 'N/A'} | SKU: ${product.sku || 'N/A'} | Name: ${product.name || 'Unnamed Product'}`;
|
||||
productsDiv.appendChild(productDiv);
|
||||
});
|
||||
|
||||
nodeDiv.appendChild(productsDiv);
|
||||
}
|
||||
|
||||
// Add children
|
||||
if (hasChildren) {
|
||||
const childrenDiv = document.createElement('div');
|
||||
childrenDiv.className = 'tree-children';
|
||||
childrenDiv.style.display = 'none';
|
||||
node.children.forEach(child => {
|
||||
childrenDiv.appendChild(createCategoryWithProductsNode(child));
|
||||
});
|
||||
nodeDiv.appendChild(childrenDiv);
|
||||
}
|
||||
|
||||
return nodeDiv;
|
||||
}
|
||||
|
||||
// Make functions available globally
|
||||
window.startProductMigration = startProductMigration;
|
||||
window.deleteM2Product = deleteM2Product;
|
||||
window.deleteProductsAboveM1Max = deleteProductsAboveM1Max;
|
||||
window.syncProductCategories = syncProductCategories;
|
||||
window.loadM1CategoryTreeWithProducts = loadM1CategoryTreeWithProducts;
|
||||
window.loadM2CategoryTreeWithProducts = loadM2CategoryTreeWithProducts;
|
||||
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Attribute Groups Section -->
|
||||
<div class="section">
|
||||
<h2>📦 Attribute Groups</h2>
|
||||
<p>View attribute groups that organize attributes within attribute sets:</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
|
||||
<!-- Magento 1 Attribute Groups -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #667eea;">Magento 1 Attribute Groups ({{ $m1AttributeGroups->count() }})</h3>
|
||||
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 600px; overflow-y: auto;">
|
||||
@if($m1AttributeGroups->count() > 0)
|
||||
<table class="attributes-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group ID</th>
|
||||
<th>Group Name</th>
|
||||
<th>Attribute Set</th>
|
||||
<th>Attributes</th>
|
||||
<th>Sort Order</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($m1AttributeGroups as $group)
|
||||
@php
|
||||
$groupKey = ($group->attribute_set_name ?? 'Default') . '|' . ($group->attribute_group_name ?? '');
|
||||
$isMissing = $m1AttributeGroupsMissingInM2->contains(function($missingGroup) use ($groupKey) {
|
||||
return ($missingGroup->attribute_set_name ?? 'Default') . '|' . ($missingGroup->attribute_group_name ?? '') === $groupKey;
|
||||
});
|
||||
@endphp
|
||||
<tr id="group-row-{{ $group->attribute_group_id }}-{{ $group->attribute_set_id }}" style="{{ $isMissing ? 'background: #fff3cd;' : '' }}">
|
||||
<td>{{ $group->attribute_group_id }}</td>
|
||||
<td>{{ $group->attribute_group_name ?? 'N/A' }}</td>
|
||||
<td>{{ $group->attribute_set_name ?? 'Default' }}</td>
|
||||
<td>{{ $group->attribute_count ?? 0 }}</td>
|
||||
<td>{{ $group->sort_order ?? 0 }}</td>
|
||||
<td>
|
||||
@if($isMissing)
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="migrateAttributeGroup({{ $group->attribute_group_id }}, {{ $group->attribute_set_id }}, '{{ $group->attribute_group_name ?? 'N/A' }}')"
|
||||
id="migrate-group-btn-{{ $group->attribute_group_id }}-{{ $group->attribute_set_id }}"
|
||||
style="padding: 6px 12px; font-size: 0.85em;">
|
||||
Migrate
|
||||
</button>
|
||||
@else
|
||||
<span style="color: #28a745;">✓ Exists</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div style="text-align: center; padding: 40px; color: #999;">
|
||||
No attribute groups found.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Magento 2 Attribute Groups -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #764ba2;">Magento 2 Attribute Groups ({{ $m2AttributeGroups->count() }})</h3>
|
||||
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 600px; overflow-y: auto;">
|
||||
@if($m2AttributeGroups->count() > 0)
|
||||
<table class="attributes-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group ID</th>
|
||||
<th>Group Name</th>
|
||||
<th>Attribute Set</th>
|
||||
<th>Attributes</th>
|
||||
<th>Sort Order</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($m2AttributeGroups as $group)
|
||||
<tr>
|
||||
<td>{{ $group->attribute_group_id }}</td>
|
||||
<td>{{ $group->attribute_group_name ?? 'N/A' }}</td>
|
||||
<td>{{ $group->attribute_set_name ?? 'Default' }}</td>
|
||||
<td>{{ $group->attribute_count ?? 0 }}</td>
|
||||
<td>{{ $group->sort_order ?? 0 }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div style="text-align: center; padding: 40px; color: #999;">
|
||||
No attribute groups found.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Missing Attributes Section -->
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<h2>⚠️ Missing Attributes</h2>
|
||||
<p>These attributes exist in Magento 1 but are missing in Magento 2:</p>
|
||||
|
||||
@if($m1AttributesMissingInM2->count() > 0)
|
||||
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 600px; overflow-y: auto; margin-top: 15px;">
|
||||
<table class="attributes-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Code</th>
|
||||
<th>Label</th>
|
||||
<th>Type</th>
|
||||
<th>Input</th>
|
||||
<th>Required</th>
|
||||
<th>User Defined</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($m1AttributesMissingInM2 as $attr)
|
||||
<tr id="attr-row-{{ $attr->attribute_id }}">
|
||||
<td>{{ $attr->attribute_id }}</td>
|
||||
<td style="font-weight: 500; color: #667eea;">{{ $attr->attribute_code }}</td>
|
||||
<td>{{ $attr->frontend_label ?? 'N/A' }}</td>
|
||||
<td style="color: #666;">{{ $attr->backend_type ?? 'N/A' }}</td>
|
||||
<td style="color: #666;">{{ $attr->frontend_input ?? 'N/A' }}</td>
|
||||
<td>
|
||||
@if($attr->is_required ?? 0)
|
||||
<span style="color: #d32f2f; font-weight: 600;">Yes</span>
|
||||
@else
|
||||
<span style="color: #666;">No</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($attr->is_user_defined ?? 0)
|
||||
<span style="color: #1976D2; font-weight: 600;">Yes</span>
|
||||
@else
|
||||
<span style="color: #666;">No</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="migrateAttribute({{ $attr->attribute_id }}, '{{ $attr->attribute_code }}')"
|
||||
id="migrate-btn-{{ $attr->attribute_id }}"
|
||||
style="padding: 6px 12px; font-size: 0.85em;">
|
||||
Migrate
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="missingAttributesCount" style="margin-top: 15px; padding: 12px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;">
|
||||
<strong>Total:</strong> <span id="missingAttributesTotal">{{ $m1AttributesMissingInM2->count() }}</span> {{ Str::plural('attribute', $m1AttributesMissingInM2->count()) }} found in Magento 1 but not in Magento 2.
|
||||
</div>
|
||||
@else
|
||||
<div style="margin-top: 15px; padding: 15px; background: #d4edda; border-left: 4px solid #28a745; border-radius: 4px; color: #155724;">
|
||||
✓ All Magento 1 attributes have matching attribute codes in Magento 2.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Category Attributes Section -->
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<h2>📋 Category Attributes</h2>
|
||||
<p>View all category attributes from Magento 1 and Magento 2</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
|
||||
<!-- Magento 1 Attributes -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #667eea;">Magento 1 Attributes ({{ $m1Attributes->count() }})</h3>
|
||||
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 600px; overflow-y: auto;">
|
||||
@if($m1Attributes->count() > 0)
|
||||
<table class="attributes-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Code</th>
|
||||
<th>Label</th>
|
||||
<th>Type</th>
|
||||
<th>Input</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($m1Attributes as $attr)
|
||||
<tr>
|
||||
<td>{{ $attr->attribute_id }}</td>
|
||||
<td style="font-weight: 500; color: #667eea;">{{ $attr->attribute_code }}</td>
|
||||
<td>{{ $attr->frontend_label ?? 'N/A' }}</td>
|
||||
<td style="color: #666;">{{ $attr->backend_type ?? 'N/A' }}</td>
|
||||
<td style="color: #666;">{{ $attr->frontend_input ?? 'N/A' }}</td>
|
||||
<td>
|
||||
@if($attr->is_required ?? 0)
|
||||
<span style="color: #d32f2f; font-weight: 600;">Yes</span>
|
||||
@else
|
||||
<span style="color: #666;">No</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div style="text-align: center; padding: 40px; color: #999;">
|
||||
No attributes found.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Magento 2 Attributes -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #764ba2;">Magento 2 Attributes ({{ $m2Attributes->count() }})</h3>
|
||||
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 600px; overflow-y: auto;">
|
||||
@if($m2Attributes->count() > 0)
|
||||
<table class="attributes-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Code</th>
|
||||
<th>Label</th>
|
||||
<th>Type</th>
|
||||
<th>Input</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($m2Attributes as $attr)
|
||||
<tr>
|
||||
<td>{{ $attr->attribute_id }}</td>
|
||||
<td style="font-weight: 500; color: #764ba2;">{{ $attr->attribute_code }}</td>
|
||||
<td>{{ $attr->frontend_label ?? 'N/A' }}</td>
|
||||
<td style="color: #666;">{{ $attr->backend_type ?? 'N/A' }}</td>
|
||||
<td style="color: #666;">{{ $attr->frontend_input ?? 'N/A' }}</td>
|
||||
<td>
|
||||
@if($attr->is_required ?? 0)
|
||||
<span style="color: #d32f2f; font-weight: 600;">Yes</span>
|
||||
@else
|
||||
<span style="color: #666;">No</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div style="text-align: center; padding: 40px; color: #999;">
|
||||
No attributes found.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
{{-- CSS is loaded globally via app.css --}}
|
||||
|
||||
@push('scripts')
|
||||
@vite(['resources/js/attributes.js'])
|
||||
<script>
|
||||
window.attributeRoutes = {
|
||||
migrateAttribute: '{{ route("attributes.migrate-attribute", ["attributeId" => ":id"]) }}',
|
||||
migrateAttributeGroup: '{{ route("attributes.migrate-attribute-group", ["groupId" => ":groupId", "setId" => ":setId"]) }}'
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Statistics -->
|
||||
<div class="section">
|
||||
<h2>📊 Category Statistics</h2>
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="number">{{ $m1CategoriesCount }}</div>
|
||||
<div class="label">Magento 1 Categories</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number">{{ $m2CategoriesCount }}</div>
|
||||
<div class="label">Magento 2 Categories</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number">{{ $m1CategoriesCount - $m2CategoriesCount }}</div>
|
||||
<div class="label">Difference</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Missing Categories Section -->
|
||||
<div class="section">
|
||||
<h2>⚠️ Magento 2 Categories Not Found in Magento 1</h2>
|
||||
<p>These categories exist in Magento 2 but do not have a matching name in Magento 1:</p>
|
||||
|
||||
@if($m2CategoriesNotInM1->count() > 0)
|
||||
<div id="m2-categories-not-in-m1-table-wrapper" style="margin-top: 15px; max-height: 400px; overflow-y: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; background: white; border-radius: 6px;">
|
||||
<thead>
|
||||
<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; color: #333;">ID</th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; color: #333;">Category Name</th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; color: #333;">Level</th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; color: #333;">Status</th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; color: #333;">Root Category</th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; color: #333;">Path</th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; color: #333;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="m2-categories-not-in-m1-tbody">
|
||||
@foreach($m2CategoriesNotInM1 as $category)
|
||||
<tr style="border-bottom: 1px solid #dee2e6;">
|
||||
<td style="padding: 10px 12px;">{{ $category->entity_id }}</td>
|
||||
<td style="padding: 10px 12px; font-weight: 500;">{{ $category->name ?? 'Unnamed Category' }}</td>
|
||||
<td style="padding: 10px 12px;">{{ $category->level ?? 'N/A' }}</td>
|
||||
<td style="padding: 10px 12px;">
|
||||
<span class="tree-badge {{ ($category->is_active ?? 0) ? 'active' : 'inactive' }}">
|
||||
{{ ($category->is_active ?? 0) ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding: 10px 12px;">
|
||||
@if(isset($category->root_category_name) && $category->root_category_name !== 'N/A')
|
||||
<span style="font-weight: 500;">{{ $category->root_category_name }}</span>
|
||||
@if(isset($category->root_category_id))
|
||||
<span style="font-size: 0.85em; color: #666;">(ID: {{ $category->root_category_id }})</span>
|
||||
@endif
|
||||
@else
|
||||
<span style="color: #999;">N/A</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="padding: 10px 12px; font-size: 0.9em; color: #666;">{{ $category->path ?? 'N/A' }}</td>
|
||||
<td style="padding: 10px 12px;">
|
||||
<button
|
||||
class="tree-delete-btn"
|
||||
onclick="showDeleteConfirmPopup(this, {{ $category->entity_id }}, '{{ addslashes($category->name ?? 'Unnamed Category') }}', 'm2', false, 0)"
|
||||
title="Delete this category">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="m2-categories-not-in-m1-total" style="margin-top: 15px; padding: 12px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;">
|
||||
<strong>Total:</strong> {{ $m2CategoriesNotInM1->count() }} {{ Str::plural('category', $m2CategoriesNotInM1->count()) }} found in Magento 2 but not in Magento 1.
|
||||
</div>
|
||||
@else
|
||||
<div id="m2-categories-not-in-m1-empty" style="margin-top: 15px; padding: 15px; background: #d4edda; border-left: 4px solid #28a745; border-radius: 4px; color: #155724;">
|
||||
✓ All Magento 2 categories have matching names in Magento 1.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🌳 Category Trees</h2>
|
||||
<p>View category hierarchies from Magento 1 and Magento 2</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
|
||||
<!-- Magento 1 Tree -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #667eea;">Magento 1 Categories</h3>
|
||||
<div class="tree-container" id="m1-tree-container">
|
||||
<div class="tree-loading">Loading categories...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Magento 2 Tree -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #764ba2;">Magento 2 Categories</h3>
|
||||
<div class="tree-container" id="m2-tree-container">
|
||||
<div class="tree-loading">Loading categories...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
{{-- CSS is loaded globally via app.css --}}
|
||||
|
||||
@push('scripts')
|
||||
@vite(['resources/js/categories.js'])
|
||||
<script>
|
||||
window.categoryRoutes = {
|
||||
magento1CategoryTree: '{{ route("categories.magento1-category-tree") }}',
|
||||
magento2CategoryTree: '{{ route("categories.magento2-category-tree") }}',
|
||||
deleteCategory: '{{ route("categories.delete-category", ["categoryId" => ":id"]) }}'
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="section">
|
||||
<h2>📡 Database Connections</h2>
|
||||
<div class="connection-status">
|
||||
<div>
|
||||
<strong>Magento 1:</strong>
|
||||
<span class="status-badge {{ $connectionTest['magento1'] ? 'success' : 'error' }}">
|
||||
{{ $connectionTest['magento1'] ? '✓ Connected' : '✗ Failed' }}
|
||||
</span>
|
||||
@if(!$connectionTest['magento1'] && isset($connectionTest['magento1_error']))
|
||||
<div style="color: #721c24; margin-top: 5px; font-size: 0.9em;">
|
||||
{{ $connectionTest['magento1_error'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<strong>Magento 2:</strong>
|
||||
<span class="status-badge {{ $connectionTest['magento2'] ? 'success' : 'error' }}">
|
||||
{{ $connectionTest['magento2'] ? '✓ Connected' : '✗ Failed' }}
|
||||
</span>
|
||||
@if(!$connectionTest['magento2'] && isset($connectionTest['magento2_error']))
|
||||
<div style="color: #721c24; margin-top: 5px; font-size: 0.9em;">
|
||||
{{ $connectionTest['magento2_error'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="testConnections()">Test Connections</button>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
{{-- CSS is loaded globally via app.css --}}
|
||||
|
||||
@push('scripts')
|
||||
@vite(['resources/js/connections.js'])
|
||||
<div data-test-route="{{ route('connections.test-connections') }}" style="display: none;"></div>
|
||||
@endpush
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Magento Migration Tool</title>
|
||||
@vite(['resources/css/app.css'])
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🛒 Magento Migration</h1>
|
||||
<p>Migrate categories from Magento 1 to Magento 2 with multi-store support</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
@include('partials.navigation')
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,18 @@
|
|||
<nav class="navigation">
|
||||
<a href="{{ route('connections.index') }}" class="nav-link {{ request()->routeIs('connections.*') ? 'active' : '' }}">
|
||||
Database Connections
|
||||
</a>
|
||||
<a href="{{ route('categories.index') }}" class="nav-link {{ request()->routeIs('categories.*') ? 'active' : '' }}">
|
||||
Category Trees
|
||||
</a>
|
||||
<a href="{{ route('migration.index') }}" class="nav-link {{ request()->routeIs('migration.*') ? 'active' : '' }}">
|
||||
Category Migration
|
||||
</a>
|
||||
<a href="{{ route('attributes.index') }}" class="nav-link {{ request()->routeIs('attributes.*') ? 'active' : '' }}">
|
||||
Attribute List
|
||||
</a>
|
||||
<a href="{{ route('products.index') }}" class="nav-link {{ request()->routeIs('products.*') ? 'active' : '' }}">
|
||||
Products
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Product Migration Section -->
|
||||
<div class="section">
|
||||
<h2>🚀 Product 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 Product Migration"?</h3>
|
||||
<p style="margin: 5px 0; color: #1976D2;">The product migration process will:</p>
|
||||
<ul style="margin: 10px 0 0 20px; color: #1976D2; line-height: 1.8;">
|
||||
<li><strong>Create new products:</strong> If a product with the same SKU doesn't exist in Magento 2, it will be created with all its attributes</li>
|
||||
<li><strong>Update existing products:</strong> If a product with the same SKU already exists in Magento 2, it will be updated with the latest data from Magento 1</li>
|
||||
<li><strong>Migrate product attributes:</strong> All product attributes including SKU, name, description, price, weight, status, visibility, and tax class will be migrated</li>
|
||||
<li><strong>Assign to categories:</strong> Products will be assigned to the corresponding categories in Magento 2</li>
|
||||
<li><strong>Generate logs:</strong> Detailed migration logs showing which products 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="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)">
|
||||
Start Product Migration
|
||||
</button>
|
||||
<button id="deleteProductsAboveM1MaxBtn" class="btn btn-danger" onclick="deleteProductsAboveM1Max()">
|
||||
Delete M2 Products Above M1 Max ID
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Missing Attributes Section -->
|
||||
<div class="section" id="missingAttributesSection" style="margin-top: 30px; display: none;">
|
||||
<h2>⚠️ Missing Attributes</h2>
|
||||
<p style="margin-bottom: 15px; color: #666;">The following attributes exist in Magento 1 but are missing in Magento 2. These need to be created before migration:</p>
|
||||
<div style="background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-height: 400px; overflow-y: auto;">
|
||||
<table class="products-table" id="missingAttributesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Attribute Code</th>
|
||||
<th>Label</th>
|
||||
<th>Backend Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="missingAttributesTableBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Migration Logs Section -->
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<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>
|
||||
<div class="log-container" id="productMigrationLogContainer" style="display: block;">
|
||||
<div id="productMigrationLogContent">
|
||||
<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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product Statistics -->
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<h2>📊 Product Statistics</h2>
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="number">{{ $m1Products->count() }}</div>
|
||||
<div class="label">Magento 1 Products</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number">{{ $m2Products->count() }}</div>
|
||||
<div class="label">Magento 2 Products</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number">{{ $m1ProductsNotInM2->count() }}</div>
|
||||
<div class="label">M1 Products Not in M2</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number">{{ $m2ProductsNotInM1->count() }}</div>
|
||||
<div class="label">M2 Products Not in M1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products Not in M2 Section -->
|
||||
@if($m1ProductsNotInM2->count() > 0)
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<h2>⚠️ Magento 1 Products Not in Magento 2</h2>
|
||||
<p>These products 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>SKU</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($m1ProductsNotInM2->take(50) as $product)
|
||||
<tr>
|
||||
<td>{{ $product->entity_id }}</td>
|
||||
<td>{{ $product->sku ?? 'N/A' }}</td>
|
||||
<td>{{ $product->name ?? 'Unnamed Product' }}</td>
|
||||
<td>{{ $product->type_id ?? 'N/A' }}</td>
|
||||
<td>
|
||||
@if(($product->status ?? 0) == 1)
|
||||
<span style="color: #28a745;">Enabled</span>
|
||||
@else
|
||||
<span style="color: #dc3545;">Disabled</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@if($m1ProductsNotInM2->count() > 50)
|
||||
<p style="margin-top: 15px; color: #666;">Showing first 50 of {{ $m1ProductsNotInM2->count() }} products.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Products Not in M1 Section -->
|
||||
@if($m2ProductsNotInM1->count() > 0)
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<h2>⚠️ Magento 2 Products Not in Magento 1</h2>
|
||||
<p>These products 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>SKU</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($m2ProductsNotInM1->take(50) as $product)
|
||||
<tr>
|
||||
<td>{{ $product->entity_id }}</td>
|
||||
<td>{{ $product->sku ?? 'N/A' }}</td>
|
||||
<td>{{ $product->name ?? 'Unnamed Product' }}</td>
|
||||
<td>{{ $product->type_id ?? 'N/A' }}</td>
|
||||
<td>
|
||||
@if(($product->status ?? 0) == 1)
|
||||
<span style="color: #28a745;">Enabled</span>
|
||||
@else
|
||||
<span style="color: #dc3545;">Disabled</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
onclick="deleteM2Product({{ $product->entity_id }}, '{{ addslashes($product->name ?? 'Unnamed Product') }}', '{{ addslashes($product->sku ?? 'N/A') }}')"
|
||||
style="padding: 6px 12px; font-size: 0.85em;">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@if($m2ProductsNotInM1->count() > 50)
|
||||
<p style="margin-top: 15px; color: #666;">Showing first 50 of {{ $m2ProductsNotInM1->count() }} products.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Sync Product Categories Section -->
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<h2>🔄 Sync Product Categories</h2>
|
||||
<p>Sync product category assignments from Magento 1 to Magento 2:</p>
|
||||
<button id="syncProductCategoriesBtn" class="btn btn-primary" onclick="syncProductCategories()" style="margin-top: 15px;">
|
||||
Sync Product Categories
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Category Tree with Products Section -->
|
||||
<div class="section" style="margin-top: 30px;">
|
||||
<h2>🌳 Category Tree with Products</h2>
|
||||
<p>View all categories with their associated products in a tree structure</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
|
||||
<!-- Magento 1 Tree with Products -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #667eea;">Magento 1 Categories</h3>
|
||||
<button onclick="loadM1CategoryTreeWithProducts()" style="padding: 10px 20px; font-size: 1em; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; margin-bottom: 15px;">
|
||||
Load M1 Category Tree with Products
|
||||
</button>
|
||||
<div class="tree-container" id="m1-category-products-tree-container">
|
||||
<div class="tree-loading">Click the button above to load categories with products</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Magento 2 Tree with Products -->
|
||||
<div>
|
||||
<h3 style="margin-bottom: 15px; color: #764ba2;">Magento 2 Categories</h3>
|
||||
<button onclick="loadM2CategoryTreeWithProducts()" style="padding: 10px 20px; font-size: 1em; background: #764ba2; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; margin-bottom: 15px;">
|
||||
Load M2 Category Tree with Products
|
||||
</button>
|
||||
<div class="tree-container" id="m2-category-products-tree-container">
|
||||
<div class="tree-loading">Click the button above to load categories with products</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
{{-- CSS is loaded globally via app.css --}}
|
||||
|
||||
@push('scripts')
|
||||
@vite(['resources/js/products.js'])
|
||||
<script>
|
||||
window.productRoutes = {
|
||||
migrateProducts: '{{ route("products.migrate-products") }}',
|
||||
deleteProduct: '{{ route("products.delete-product", ["productId" => ":id"]) }}',
|
||||
deleteProductsAboveM1Max: '{{ route("products.delete-products-above-m1-max") }}',
|
||||
syncProductCategories: '{{ route("products.sync-product-categories") }}',
|
||||
magento1CategoryTreeWithProducts: '{{ route("categories.magento1-category-tree-with-products") }}',
|
||||
magento2CategoryTreeWithProducts: '{{ route("categories.magento2-category-tree-with-products") }}'
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
|
|
@ -1,28 +1,53 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\MagentoMigrationController;
|
||||
use App\Http\Controllers\ConnectionsController;
|
||||
use App\Http\Controllers\CategoriesController;
|
||||
use App\Http\Controllers\MigrationController;
|
||||
use App\Http\Controllers\AttributesController;
|
||||
use App\Http\Controllers\ProductsController;
|
||||
|
||||
Route::get('/', function () {
|
||||
return redirect('/migration');
|
||||
return redirect('/connections');
|
||||
});
|
||||
|
||||
Route::prefix('migration')->group(function () {
|
||||
Route::get('/', [MagentoMigrationController::class, 'index'])->name('migration.index');
|
||||
Route::post('/migrate', [MagentoMigrationController::class, 'migrate'])->name('migration.migrate');
|
||||
Route::get('/test-connections', [MagentoMigrationController::class, 'testConnections'])->name('migration.test-connections');
|
||||
Route::get('/magento1-categories', [MagentoMigrationController::class, 'getMagento1Categories'])->name('migration.magento1-categories');
|
||||
Route::get('/magento1-category-tree', [MagentoMigrationController::class, 'getMagento1CategoryTree'])->name('migration.magento1-category-tree');
|
||||
Route::get('/magento1-category-tree-with-products', [MagentoMigrationController::class, 'getMagento1CategoryTreeWithProducts'])->name('migration.magento1-category-tree-with-products');
|
||||
Route::get('/magento2-category-tree', [MagentoMigrationController::class, 'getMagento2CategoryTree'])->name('migration.magento2-category-tree');
|
||||
Route::get('/magento2-category-tree-with-products', [MagentoMigrationController::class, 'getMagento2CategoryTreeWithProducts'])->name('migration.magento2-category-tree-with-products');
|
||||
Route::get('/m2-categories-not-in-m1', [MagentoMigrationController::class, 'getM2CategoriesNotInM1'])->name('migration.m2-categories-not-in-m1');
|
||||
Route::delete('/category/{categoryId}', [MagentoMigrationController::class, 'deleteCategory'])->name('migration.delete-category');
|
||||
Route::put('/category/{categoryId}/rename', [MagentoMigrationController::class, 'renameCategory'])->name('migration.rename-category');
|
||||
Route::post('/attribute/{attributeId}/migrate', [MagentoMigrationController::class, 'migrateAttribute'])->name('migration.migrate-attribute');
|
||||
Route::post('/attribute-group/{groupId}/{setId}/migrate', [MagentoMigrationController::class, 'migrateAttributeGroup'])->name('migration.migrate-attribute-group');
|
||||
Route::post('/products/migrate', [MagentoMigrationController::class, 'migrateProducts'])->name('migration.migrate-products');
|
||||
Route::post('/products/sync-categories', [MagentoMigrationController::class, 'syncProductCategories'])->name('migration.sync-product-categories');
|
||||
Route::delete('/products/{productId}', [MagentoMigrationController::class, 'deleteM2Product'])->name('migration.delete-product');
|
||||
Route::delete('/products/above-m1-max', [MagentoMigrationController::class, 'deleteM2ProductsAboveM1Max'])->name('migration.delete-products-above-m1-max');
|
||||
// Connections routes
|
||||
Route::prefix('connections')->name('connections.')->group(function () {
|
||||
Route::get('/', [ConnectionsController::class, 'index'])->name('index');
|
||||
Route::get('/test', [ConnectionsController::class, 'testConnections'])->name('test-connections');
|
||||
});
|
||||
|
||||
// Categories routes
|
||||
Route::prefix('categories')->name('categories.')->group(function () {
|
||||
Route::get('/', [CategoriesController::class, 'index'])->name('index');
|
||||
Route::get('/magento1', [CategoriesController::class, 'getMagento1Categories'])->name('magento1-categories');
|
||||
Route::get('/magento1-tree', [CategoriesController::class, 'getMagento1CategoryTree'])->name('magento1-category-tree');
|
||||
Route::get('/magento1-tree-with-products', [CategoriesController::class, 'getMagento1CategoryTreeWithProducts'])->name('magento1-category-tree-with-products');
|
||||
Route::get('/magento2-tree', [CategoriesController::class, 'getMagento2CategoryTree'])->name('magento2-category-tree');
|
||||
Route::get('/magento2-tree-with-products', [CategoriesController::class, 'getMagento2CategoryTreeWithProducts'])->name('magento2-category-tree-with-products');
|
||||
Route::get('/m2-not-in-m1', [CategoriesController::class, 'getM2CategoriesNotInM1'])->name('m2-categories-not-in-m1');
|
||||
Route::delete('/{categoryId}', [CategoriesController::class, 'deleteCategory'])->name('delete-category');
|
||||
Route::put('/{categoryId}/rename', [CategoriesController::class, 'renameCategory'])->name('rename-category');
|
||||
});
|
||||
|
||||
// Migration routes
|
||||
Route::prefix('migration')->name('migration.')->group(function () {
|
||||
Route::get('/', [MigrationController::class, 'index'])->name('index');
|
||||
Route::post('/migrate', [MigrationController::class, 'migrate'])->name('migrate');
|
||||
});
|
||||
|
||||
// Attributes routes
|
||||
Route::prefix('attributes')->name('attributes.')->group(function () {
|
||||
Route::get('/', [AttributesController::class, 'index'])->name('index');
|
||||
Route::post('/{attributeId}/migrate', [AttributesController::class, 'migrateAttribute'])->name('migrate-attribute');
|
||||
Route::post('/group/{groupId}/{setId}/migrate', [AttributesController::class, 'migrateAttributeGroup'])->name('migrate-attribute-group');
|
||||
});
|
||||
|
||||
// Products routes
|
||||
Route::prefix('products')->name('products.')->group(function () {
|
||||
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::delete('/{productId}', [ProductsController::class, 'deleteM2Product'])->name('delete-product');
|
||||
Route::delete('/above-m1-max', [ProductsController::class, 'deleteM2ProductsAboveM1Max'])->name('delete-products-above-m1-max');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,15 @@ import tailwindcss from '@tailwindcss/vite';
|
|||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
input: [
|
||||
'resources/css/app.css',
|
||||
'resources/js/app.js',
|
||||
'resources/js/connections.js',
|
||||
'resources/js/categories.js',
|
||||
'resources/js/migration.js',
|
||||
'resources/js/attributes.js',
|
||||
'resources/js/products.js',
|
||||
],
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue