diff --git a/app/Http/Controllers/MagentoMigrationController.php b/app/Http/Controllers/MagentoMigrationController.php index ae8e16e..62e0f70 100644 --- a/app/Http/Controllers/MagentoMigrationController.php +++ b/app/Http/Controllers/MagentoMigrationController.php @@ -32,6 +32,10 @@ public function index() $m1AttributeGroups = $this->migrationService->getMagento1AttributeGroups(); $m2AttributeGroups = $this->migrationService->getMagento2AttributeGroups(); $m1AttributeGroupsMissingInM2 = $this->migrationService->getM1AttributeGroupsMissingInM2(); + $m1Products = $this->migrationService->getMagento1Products(); + $m2Products = $this->migrationService->getMagento2Products(); + $m1ProductsNotInM2 = $this->migrationService->getM1ProductsNotInM2(); + $m2ProductsNotInM1 = $this->migrationService->getM2ProductsNotInM1(); return view('migration.index', [ 'm1Stores' => $m1Stores, @@ -46,6 +50,10 @@ public function index() 'm1AttributeGroups' => $m1AttributeGroups, 'm2AttributeGroups' => $m2AttributeGroups, 'm1AttributeGroupsMissingInM2' => $m1AttributeGroupsMissingInM2, + 'm1Products' => $m1Products, + 'm2Products' => $m2Products, + 'm1ProductsNotInM2' => $m1ProductsNotInM2, + 'm2ProductsNotInM1' => $m2ProductsNotInM1, ]); } @@ -106,6 +114,50 @@ public function getMagento1CategoryTree() } } + /** + * 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 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 Magento 2 category tree */ @@ -220,6 +272,97 @@ public function migrateAttributeGroup(Request $request, $groupId, $setId) } } + /** + * 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); + } + } + /** * Execute the migration */ diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index df05442..39dc887 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -1479,6 +1479,1805 @@ public function migrateAttributeGroup($m1GroupId, $m1SetId) } } + /** + * Get all products from Magento 1 + */ + public function getMagento1Products() + { + try { + // Get entity type ID for catalog_product + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return collect([]); + } + + // Get base product data - check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + // SKU column doesn't exist, will use EAV + $hasSkuColumn = false; + } + + if ($hasSkuColumn) { + // SKU is stored directly in entity table + $products = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('entity_id', 'sku', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } else { + // SKU is stored as EAV attribute + $products = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('entity_id', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } + + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKU and name values from EAV if needed + $skus = []; + $names = []; + + if (!$hasSkuColumn && $skuAttributeId) { + $skuValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Combine data + foreach ($products as $product) { + if ($hasSkuColumn) { + // SKU from entity table + $product->sku = !empty($product->sku) ? $product->sku : 'N/A'; + } else { + // SKU from EAV + $product->sku = $skus[$product->entity_id] ?? 'N/A'; + } + $product->name = $names[$product->entity_id] ?? 'Unnamed Product'; + } + + return $products; + } catch (Exception $e) { + Log::error('Error fetching Magento 1 products: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Get all products from Magento 2 + */ + public function getMagento2Products() + { + try { + // Get entity type ID for catalog_product + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return collect([]); + } + + // Get base product data - check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + // SKU column doesn't exist, will use EAV + $hasSkuColumn = false; + } + + if ($hasSkuColumn) { + // SKU is stored directly in entity table + $products = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('entity_id', 'sku', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } else { + // SKU is stored as EAV attribute + $products = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('entity_id', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } + + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKU and name values from EAV if needed + $skus = []; + $names = []; + + if (!$hasSkuColumn && $skuAttributeId) { + $skuValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Combine data + foreach ($products as $product) { + if ($hasSkuColumn) { + // SKU from entity table + $product->sku = !empty($product->sku) ? $product->sku : 'N/A'; + } else { + // SKU from EAV + $product->sku = $skus[$product->entity_id] ?? 'N/A'; + } + $product->name = $names[$product->entity_id] ?? 'Unnamed Product'; + } + + return $products; + } catch (Exception $e) { + Log::error('Error fetching Magento 2 products: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Get Magento 1 products that don't exist in Magento 2 + */ + public function getM1ProductsNotInM2() + { + try { + $m1Products = $this->getMagento1Products(); + $m2Products = $this->getMagento2Products(); + + // Get all M2 SKUs + $m2Skus = $m2Products->pluck('sku')->filter(function($sku) { + return $sku !== 'N/A' && !empty($sku); + })->toArray(); + + // Get M2 product IDs (for products without SKU) + $m2ProductIds = $m2Products->pluck('entity_id')->toArray(); + + // Filter M1 products that don't exist in M2 + $missingProducts = $m1Products->filter(function ($m1Product) use ($m2Skus, $m2ProductIds) { + $m1Sku = $m1Product->sku ?? 'N/A'; + $hasSku = ($m1Sku !== 'N/A' && !empty($m1Sku)); + + if ($hasSku) { + // Check by SKU + return !in_array($m1Sku, $m2Skus); + } else { + // Check by product ID + return !in_array($m1Product->entity_id, $m2ProductIds); + } + }); + + return $missingProducts->values(); + } catch (Exception $e) { + Log::error('Error fetching missing products: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Get Magento 2 products that don't exist in Magento 1 + */ + public function getM2ProductsNotInM1() + { + try { + $m1Products = $this->getMagento1Products(); + $m2Products = $this->getMagento2Products(); + + // Get all M1 SKUs + $m1Skus = $m1Products->pluck('sku')->filter(function($sku) { + return $sku !== 'N/A' && !empty($sku); + })->toArray(); + + // Get M1 product IDs (for products without SKU) + $m1ProductIds = $m1Products->pluck('entity_id')->toArray(); + + // Filter M2 products that don't exist in M1 + $missingProducts = $m2Products->filter(function ($m2Product) use ($m1Skus, $m1ProductIds) { + $m2Sku = $m2Product->sku ?? 'N/A'; + $hasSku = ($m2Sku !== 'N/A' && !empty($m2Sku)); + + if ($hasSku) { + // Check by SKU + return !in_array($m2Sku, $m1Skus); + } else { + // Check by product ID + return !in_array($m2Product->entity_id, $m1ProductIds); + } + }); + + return $missingProducts->values(); + } catch (Exception $e) { + Log::error('Error fetching M2 products not in M1: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Migrate all products from Magento 1 to Magento 2 + */ + public function migrateProducts($dryRun = false) + { + try { + $this->migrationLog = []; + $addedCount = 0; + $updatedCount = 0; + $errorCount = 0; + $missingAttributes = []; + + // Get M1 and M2 entity type IDs + $m1EntityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + $m2EntityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$m1EntityTypeId || !$m2EntityTypeId) { + return [ + 'success' => false, + 'message' => 'Entity type not found', + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [], + 'missing_attributes' => [] + ]; + } + + // Get all M1 products + $m1Products = $this->getMagento1Products(); + + // Get M2 attribute IDs (common ones) + $m2AttributeIds = $this->getMagento2ProductAttributeIds(); + + // Get M1 attribute IDs for common attributes + $m1AttributeIds = $this->getMagento1ProductAttributeIds(); + + // Get all M1 attributes to check for missing ones + $allM1Attributes = $this->getAllMagento1ProductAttributes(); + + // Also get all M1 attribute IDs (not just common ones) for comprehensive checking + $allM1AttributeIds = []; + if ($allM1Attributes) { + foreach ($allM1Attributes as $attr) { + $allM1AttributeIds[$attr->attribute_code] = $attr->attribute_id; + } + } + + // Get all M2 attribute IDs for comparison + $allM2AttributeIds = $this->getAllMagento2ProductAttributeIds(); + + // Get category mapping (from previous category migrations) + $categoryMapping = $this->getCategoryMapping(); + + if (!$dryRun) { + DB::connection($this->magento2Connection)->beginTransaction(); + } + + // Check if M2 has SKU column in entity table + $m2HasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $m2HasSkuColumn = true; + } catch (Exception $e) { + $m2HasSkuColumn = false; + } + + foreach ($m1Products as $m1Product) { + try { + $m1Sku = $m1Product->sku ?? 'N/A'; + $hasSku = ($m1Sku !== 'N/A' && !empty($m1Sku)); + + // Check if product exists in M2 by SKU (if SKU exists) or by ID (if no SKU) + $m2Product = null; + $m2ProductId = null; + $isNew = false; + + if ($hasSku) { + // Check if SKU exists in M2 - try both entity table and EAV + if ($m2HasSkuColumn) { + // SKU is stored in entity table + $m2Product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('sku', $m1Sku) + ->first(); + } else { + // SKU is stored in EAV table + if (isset($m2AttributeIds['sku'])) { + $m2Product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $m2AttributeIds['sku']) + ->where('value', $m1Sku) + ->where('store_id', 0) + ->first(); + } + } + } else { + // Product has no SKU, check by entity_id + $m2Product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $m1Product->entity_id) + ->first(); + } + + if ($m2Product) { + // Product exists, get its entity_id + $m2ProductId = $m2Product->entity_id; + if (!$dryRun) { + $this->migrationLog[] = "Updating existing product: " . ($hasSku ? "SKU {$m1Sku}" : "No SKU") . " (ID: {$m2ProductId})"; + } else { + $this->migrationLog[] = "Would update existing product: " . ($hasSku ? "SKU {$m1Sku}" : "No SKU") . " (ID: {$m2ProductId})"; + } + $updatedCount++; + } else { + // Product doesn't exist + if ($hasSku) { + // Product has SKU, will get new ID + if (!$dryRun) { + // Create new product - check if we need to set SKU in entity table or EAV + if ($m2HasSkuColumn) { + // SKU goes in entity table + $m2ProductId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->insertGetId([ + 'sku' => $m1Sku, + 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, + 'type_id' => $m1Product->type_id ?? 'simple', + 'created_at' => $m1Product->created_at ?? now(), + 'updated_at' => now(), + ]); + } else { + // SKU goes in EAV table + $m2ProductId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->insertGetId([ + 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, + 'type_id' => $m1Product->type_id ?? 'simple', + 'created_at' => $m1Product->created_at ?? now(), + 'updated_at' => now(), + ]); + + // Insert SKU in EAV table + if (isset($m2AttributeIds['sku'])) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->insert([ + 'attribute_id' => $m2AttributeIds['sku'], + 'store_id' => 0, + 'entity_id' => $m2ProductId, + 'value' => $m1Sku, + ]); + } + } + $this->migrationLog[] = "Created new product: SKU {$m1Sku} (ID: {$m2ProductId})"; + } else { + $m2ProductId = $m1Product->entity_id; // Use M1 ID for dry run simulation + $this->migrationLog[] = "Would create new product: SKU {$m1Sku} (ID: {$m2ProductId})"; + } + } else { + // Product has no SKU, use M1 product ID + $m2ProductId = $m1Product->entity_id; + if (!$dryRun) { + // Check if this ID already exists in M2 + $existing = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $m2ProductId) + ->exists(); + + if (!$existing) { + $insertData = [ + 'entity_id' => $m2ProductId, + 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, + 'type_id' => $m1Product->type_id ?? 'simple', + 'created_at' => $m1Product->created_at ?? now(), + 'updated_at' => now(), + ]; + + // If SKU column exists, set it to NULL or empty + if ($m2HasSkuColumn) { + $insertData['sku'] = null; + } + + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->insert($insertData); + $this->migrationLog[] = "Created new product: No SKU (ID: {$m2ProductId}, using M1 ID)"; + } else { + $this->migrationLog[] = "Updating existing product: No SKU (ID: {$m2ProductId})"; + $updatedCount++; + $addedCount--; // Adjust counts + continue; + } + } else { + $this->migrationLog[] = "Would create new product: No SKU (ID: {$m2ProductId}, using M1 ID)"; + } + } + $isNew = true; + $addedCount++; + } + + // Migrate product attributes (with dry run support) + // Use all M1 attributes for comprehensive checking, not just common ones + $missingAttrs = $this->migrateProductAttributes($m1Product->entity_id, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $allM1AttributeIds, $allM2AttributeIds, $isNew, $dryRun, $allM1Attributes); + if ($missingAttrs) { + foreach ($missingAttrs as $attr) { + // Check if this attribute is already in the list + $exists = false; + foreach ($missingAttributes as $existingAttr) { + if ($existingAttr['code'] === $attr['code']) { + $exists = true; + break; + } + } + if (!$exists) { + $missingAttributes[] = $attr; + } + } + } + + // Migrate category associations (skip in dry run) + if (!$dryRun) { + // Ensure product is enabled and visible (required for products to show in categories after reindex) + $this->ensureProductIsEnabledAndVisible($m2ProductId); + $this->migrateProductCategories($m1Product->entity_id, $m2ProductId, $categoryMapping); + } + + } catch (Exception $e) { + $errorCount++; + $errorMsg = $e->getMessage(); + Log::error("Error migrating product {$m1Product->entity_id} (SKU: " . ($m1Product->sku ?? 'N/A') . "): " . $errorMsg); + $this->migrationLog[] = "ERROR: Failed to migrate product SKU " . ($m1Product->sku ?? 'N/A') . ": " . $errorMsg; + + // Try to extract missing attribute from error message + if (preg_match("/Table.*catalog_product_entity_(\w+).*doesn't exist/", $errorMsg, $matches)) { + $tableType = $matches[1] ?? null; + if ($tableType && $allM1Attributes) { + // Find attributes that use this backend type + foreach ($allM1Attributes as $attr) { + if ($attr->backend_type === $tableType) { + $missingAttr = [ + 'code' => $attr->attribute_code, + 'label' => $attr->frontend_label ?? $attr->attribute_code, + 'type' => $attr->backend_type + ]; + // Check if already exists + $exists = false; + foreach ($missingAttributes as $existingAttr) { + if ($existingAttr['code'] === $missingAttr['code']) { + $exists = true; + break; + } + } + if (!$exists) { + $missingAttributes[] = $missingAttr; + } + } + } + } + } + } + } + + if (!$dryRun) { + DB::connection($this->magento2Connection)->commit(); + } + + return [ + 'success' => true, + 'message' => $dryRun ? 'Product migration dry run completed' : 'Product migration completed', + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog, + 'missing_attributes' => $missingAttributes, + 'dry_run' => $dryRun + ]; + + } catch (Exception $e) { + if (!$dryRun && isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Product migration error: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Product migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [], + 'missing_attributes' => [] + ]; + } + } + + /** + * Get Magento 1 product attribute IDs + */ + protected function getMagento1ProductAttributeIds() + { + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return []; + } + + $attributes = ['sku', 'name', 'description', 'short_description', 'price', 'weight', 'status', 'visibility', 'tax_class_id']; + $attributeIds = []; + + foreach ($attributes as $attrCode) { + $attrId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', $attrCode) + ->value('attribute_id'); + if ($attrId) { + $attributeIds[$attrCode] = $attrId; + } + } + + return $attributeIds; + } + + /** + * Get all Magento 1 product attributes + */ + protected function getAllMagento1ProductAttributes() + { + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return collect([]); + } + + return DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_label') + ->get(); + } + + /** + * Get all Magento 2 product attribute IDs + */ + protected function getAllMagento2ProductAttributeIds() + { + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return []; + } + + $attributes = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->select('attribute_id', 'attribute_code') + ->get(); + + $attributeIds = []; + foreach ($attributes as $attr) { + $attributeIds[$attr->attribute_code] = $attr->attribute_id; + } + + return $attributeIds; + } + + /** + * Get Magento 2 product attribute IDs + */ + protected function getMagento2ProductAttributeIds() + { + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return []; + } + + $attributes = ['sku', 'name', 'description', 'short_description', 'price', 'weight', 'status', 'visibility', 'tax_class_id']; + $attributeIds = []; + + foreach ($attributes as $attrCode) { + $attrId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', $attrCode) + ->value('attribute_id'); + if ($attrId) { + $attributeIds[$attrCode] = $attrId; + } + } + + return $attributeIds; + } + + /** + * Migrate product attributes from M1 to M2 + */ + protected function migrateProductAttributes($m1ProductId, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $m1AttributeIds, $m2AttributeIds, $isNew, $dryRun = false, $allM1Attributes = null) + { + $missingAttributes = []; + + // Get attribute backend types for all M1 attributes + $m1AttributeTypes = []; + if (!empty($m1AttributeIds)) { + $m1AttributeTypes = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $m1EntityTypeId) + ->whereIn('attribute_id', array_values($m1AttributeIds)) + ->pluck('backend_type', 'attribute_id') + ->toArray(); + } + + $m2AttributeTypes = []; + if (!empty($m2AttributeIds)) { + $m2AttributeTypes = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $m2EntityTypeId) + ->whereIn('attribute_id', array_values($m2AttributeIds)) + ->pluck('backend_type', 'attribute_id') + ->toArray(); + } + + // Migrate each attribute + foreach ($m1AttributeIds as $attrCode => $m1AttrId) { + // Check if attribute exists in M2 + if (!isset($m2AttributeIds[$attrCode])) { + // Attribute doesn't exist in M2 + if ($allM1Attributes) { + $attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId); + if ($attr) { + $missingAttributes[] = [ + 'code' => $attrCode, + 'label' => $attr->frontend_label ?? $attrCode, + 'type' => $attr->backend_type ?? 'varchar' + ]; + } + } + continue; + } + + $m2AttrId = $m2AttributeIds[$attrCode]; + $backendType = $m1AttributeTypes[$m1AttrId] ?? 'varchar'; + + // Handle static attributes (stored in main entity table) + if ($backendType === 'static') { + // Static attributes are columns in catalog_product_entity table + // Skip for now as they're usually handled during entity creation + continue; + } + + // Get M1 attribute value + $m1Table = $this->magento1Prefix . 'catalog_product_entity_' . $backendType; + + try { + $m1Value = DB::connection($this->magento1Connection) + ->table($m1Table) + ->where('entity_id', $m1ProductId) + ->where('attribute_id', $m1AttrId) + ->where('store_id', 0) + ->value('value'); + } catch (Exception $e) { + // Table doesn't exist - this means the attribute table is missing + // Track this as a missing attribute/table + if ($allM1Attributes) { + $attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId); + if ($attr) { + $missingAttr = [ + 'code' => $attrCode, + 'label' => $attr->frontend_label ?? $attrCode, + 'type' => $backendType + ]; + // Check if already in list + $exists = false; + foreach ($missingAttributes as $existingAttr) { + if ($existingAttr['code'] === $missingAttr['code']) { + $exists = true; + break; + } + } + if (!$exists) { + $missingAttributes[] = $missingAttr; + } + } + } + Log::warning("Table {$m1Table} doesn't exist for attribute {$attrCode}: " . $e->getMessage()); + continue; + } + + if ($m1Value === null) { + continue; // No value in M1 + } + + if (!$dryRun) { + // Insert or update in M2 + $m2Table = $this->magento2Prefix . 'catalog_product_entity_' . $backendType; + $exists = DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $m2AttrId) + ->where('store_id', 0) + ->exists(); + + if ($exists) { + DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $m2AttrId) + ->where('store_id', 0) + ->update(['value' => $m1Value]); + } else { + DB::connection($this->magento2Connection) + ->table($m2Table) + ->insert([ + 'attribute_id' => $m2AttrId, + 'store_id' => 0, + 'entity_id' => $m2ProductId, + 'value' => $m1Value, + ]); + } + } + } + + return $missingAttributes; + } + + /** + * Delete a single product from Magento 2 + */ + public function deleteM2Product($productId) + { + try { + // Check if product exists + $product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $productId) + ->first(); + + if (!$product) { + return [ + 'success' => false, + 'message' => 'Product not found in Magento 2' + ]; + } + + DB::connection($this->magento2Connection)->beginTransaction(); + + try { + // Delete product attributes + $attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime']; + foreach ($attributeTables as $tableType) { + $table = $this->magento2Prefix . 'catalog_product_entity_' . $tableType; + try { + DB::connection($this->magento2Connection) + ->table($table) + ->where('entity_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + Log::warning("Table {$table} might not exist: " . $e->getMessage()); + } + } + + // Delete category associations + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + Log::warning("Table {$this->magento2Prefix}catalog_category_product might not exist: " . $e->getMessage()); + } + + // Delete stock items if they exist + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'cataloginventory_stock_item') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + } + + // Delete the product entity + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $productId) + ->delete(); + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => "Product ID {$productId} deleted successfully" + ]; + + } catch (Exception $e) { + DB::connection($this->magento2Connection)->rollBack(); + throw $e; + } + + } catch (Exception $e) { + Log::error('Error deleting M2 product: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to delete product: ' . $e->getMessage() + ]; + } + } + + /** + * Sync product category assignments from M1 to M2 + * Updates M2 products to be in the same categories as their M1 counterparts + */ + public function syncProductCategories() + { + try { + $this->migrationLog = []; + $updatedCount = 0; + $skippedCount = 0; + $errorCount = 0; + + // Get category mapping + $categoryMapping = $this->getCategoryMapping(); + + if (empty($categoryMapping)) { + return [ + 'success' => false, + 'message' => 'No category mapping found. Please migrate categories first.', + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + + // Get all M1 products with their category associations + $m1Products = $this->getMagento1Products(); + $m2Products = $this->getMagento2Products(); + + // Create lookup maps for M2 products + $m2ProductBySku = []; + $m2ProductById = []; + foreach ($m2Products as $m2Product) { + $sku = $m2Product->sku ?? 'N/A'; + if ($sku !== 'N/A' && !empty($sku)) { + $m2ProductBySku[$sku] = $m2Product->entity_id; + } + $m2ProductById[$m2Product->entity_id] = $m2Product->entity_id; + } + + DB::connection($this->magento2Connection)->beginTransaction(); + + foreach ($m1Products as $m1Product) { + try { + // Find corresponding M2 product + $m2ProductId = null; + $m1Sku = $m1Product->sku ?? 'N/A'; + + if ($m1Sku !== 'N/A' && !empty($m1Sku)) { + // Match by SKU + if (isset($m2ProductBySku[$m1Sku])) { + $m2ProductId = $m2ProductBySku[$m1Sku]; + } + } else { + // Match by product ID + if (isset($m2ProductById[$m1Product->entity_id])) { + $m2ProductId = $m1Product->entity_id; + } + } + + if (!$m2ProductId) { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}) - not found in M2"; + continue; + } + + // Ensure product is enabled and visible (required for products to show in categories after reindex) + $this->ensureProductIsEnabledAndVisible($m2ProductId); + + // Get M1 product categories + $m1Categories = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_category_product') + ->where('product_id', $m1Product->entity_id) + ->get(); + + if ($m1Categories->isEmpty()) { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}) - no categories in M1"; + continue; + } + + // Get current M2 product categories + $m2CurrentCategories = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $m2ProductId) + ->pluck('category_id') + ->toArray(); + + // Build expected M2 categories from M1 categories using mapping + $expectedM2Categories = []; + $categoriesToAdd = []; + $unmappedCategories = []; + + foreach ($m1Categories as $m1Category) { + if (isset($categoryMapping[$m1Category->category_id])) { + $m2CategoryId = $categoryMapping[$m1Category->category_id]; + $expectedM2Categories[] = $m2CategoryId; + + // Check if this category association needs to be added + if (!in_array($m2CategoryId, $m2CurrentCategories)) { + $categoriesToAdd[] = [ + 'category_id' => $m2CategoryId, + 'product_id' => $m2ProductId, + 'position' => $m1Category->position ?? 0, + ]; + } + } else { + // Category not in mapping - try to find it by name + $m1CategoryName = $this->getCategoryNameById($m1Category->category_id, 'm1'); + if ($m1CategoryName) { + $m2CategoryId = $this->findCategoryByName($m1CategoryName, 'm2'); + if ($m2CategoryId) { + // Found by name, add to mapping for future use + $categoryMapping[$m1Category->category_id] = $m2CategoryId; + $expectedM2Categories[] = $m2CategoryId; + + if (!in_array($m2CategoryId, $m2CurrentCategories)) { + $categoriesToAdd[] = [ + 'category_id' => $m2CategoryId, + 'product_id' => $m2ProductId, + 'position' => $m1Category->position ?? 0, + ]; + } + $this->migrationLog[] = "FOUND BY NAME: M1 Category ID {$m1Category->category_id} ({$m1CategoryName}) -> M2 Category ID {$m2CategoryId}"; + } else { + $unmappedCategories[] = $m1CategoryName ?: "ID {$m1Category->category_id}"; + } + } else { + $unmappedCategories[] = "ID {$m1Category->category_id}"; + } + } + } + + if (!empty($unmappedCategories)) { + $this->migrationLog[] = "WARNING: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - M1 categories not found in M2: " . implode(', ', $unmappedCategories); + } + + // Remove categories that are in M2 but not in M1 (after mapping) + $categoriesToRemove = array_diff($m2CurrentCategories, $expectedM2Categories); + + // Add missing category associations + foreach ($categoriesToAdd as $categoryData) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->insert($categoryData); + } + + // Remove categories that shouldn't be there + if (!empty($categoriesToRemove)) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $m2ProductId) + ->whereIn('category_id', $categoriesToRemove) + ->delete(); + } + + if (!empty($categoriesToAdd) || !empty($categoriesToRemove)) { + $updatedCount++; + $addedCount = count($categoriesToAdd); + $removedCount = count($categoriesToRemove); + $this->migrationLog[] = "UPDATED: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - Added {$addedCount} category(ies), Removed {$removedCount} category(ies)"; + } else { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - categories already match"; + } + + } catch (Exception $e) { + $errorCount++; + $m1Sku = $m1Product->sku ?? 'N/A'; + $this->migrationLog[] = "ERROR: Failed to sync categories for M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}): " . $e->getMessage(); + Log::error("Error syncing product categories for M1 Product ID {$m1Product->entity_id}: " . $e->getMessage()); + } + } + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => "Synced product categories. Updated: {$updatedCount}, Skipped: {$skippedCount}, Errors: {$errorCount}", + 'updated' => $updatedCount, + 'skipped' => $skippedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog + ]; + + } catch (Exception $e) { + if (isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Error syncing product categories: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to sync product categories: ' . $e->getMessage(), + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => $this->migrationLog + ]; + } + } + + /** + * Delete all products in Magento 2 that have entity_id greater than max M1 product ID + */ + public function deleteM2ProductsAboveM1Max() + { + try { + // Get max product ID from M1 + $maxM1ProductId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->max('entity_id'); + + if (!$maxM1ProductId) { + return [ + 'success' => false, + 'message' => 'No products found in Magento 1', + 'deleted' => 0 + ]; + } + + // Get products to delete from M2 + $productsToDelete = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', '>', $maxM1ProductId) + ->pluck('entity_id') + ->toArray(); + + if (empty($productsToDelete)) { + return [ + 'success' => true, + 'message' => 'No products found to delete', + 'deleted' => 0, + 'max_m1_id' => $maxM1ProductId + ]; + } + + $deletedCount = 0; + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + DB::connection($this->magento2Connection)->beginTransaction(); + + foreach ($productsToDelete as $productId) { + try { + // Delete product attributes + $attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime']; + foreach ($attributeTables as $tableType) { + $table = $this->magento2Prefix . 'catalog_product_entity_' . $tableType; + try { + DB::connection($this->magento2Connection) + ->table($table) + ->where('entity_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + Log::warning("Table {$table} might not exist: " . $e->getMessage()); + } + } + + // Delete category associations + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + Log::warning("Table {$this->magento2Prefix}catalog_category_product might not exist: " . $e->getMessage()); + } + + // Delete stock items if they exist + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'cataloginventory_stock_item') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + } + + // Delete the product entity + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $productId) + ->delete(); + + $deletedCount++; + } catch (Exception $e) { + Log::error("Error deleting product {$productId}: " . $e->getMessage()); + } + } + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => "Deleted {$deletedCount} product(s) with ID > {$maxM1ProductId}", + 'deleted' => $deletedCount, + 'max_m1_id' => $maxM1ProductId + ]; + + } catch (Exception $e) { + if (isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Error deleting M2 products above M1 max: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to delete products: ' . $e->getMessage(), + 'deleted' => 0 + ]; + } + } + + /** + * Migrate product category associations + */ + protected function migrateProductCategories($m1ProductId, $m2ProductId, $categoryMapping) + { + // Get M1 product categories + $m1Categories = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_category_product') + ->where('product_id', $m1ProductId) + ->get(); + + foreach ($m1Categories as $m1Category) { + // Find corresponding M2 category + if (isset($categoryMapping[$m1Category->category_id])) { + $m2CategoryId = $categoryMapping[$m1Category->category_id]; + + // Check if association already exists + $exists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('category_id', $m2CategoryId) + ->where('product_id', $m2ProductId) + ->exists(); + + if (!$exists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->insert([ + 'category_id' => $m2CategoryId, + 'product_id' => $m2ProductId, + 'position' => $m1Category->position ?? 0, + ]); + } + } + } + } + + /** + * Ensure product is enabled and visible (required for products to appear in categories after reindex) + */ + protected function ensureProductIsEnabledAndVisible($productId) + { + try { + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return; + } + + // Get attribute IDs + $statusAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'status') + ->value('attribute_id'); + + $visibilityAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'visibility') + ->value('attribute_id'); + + // Set status to 1 (enabled) if not already set + if ($statusAttributeId) { + $statusTable = $this->magento2Prefix . 'catalog_product_entity_int'; + $hasStatus = DB::connection($this->magento2Connection) + ->table($statusTable) + ->where('entity_id', $productId) + ->where('attribute_id', $statusAttributeId) + ->where('store_id', 0) + ->exists(); + + if (!$hasStatus) { + DB::connection($this->magento2Connection) + ->table($statusTable) + ->insert([ + 'attribute_id' => $statusAttributeId, + 'store_id' => 0, + 'entity_id' => $productId, + 'value' => 1, // Enabled + ]); + } else { + // Update to enabled if it's disabled + DB::connection($this->magento2Connection) + ->table($statusTable) + ->where('entity_id', $productId) + ->where('attribute_id', $statusAttributeId) + ->where('store_id', 0) + ->update(['value' => 1]); + } + } + + // Set visibility to 4 (Catalog, Search) if not already set + if ($visibilityAttributeId) { + $visibilityTable = $this->magento2Prefix . 'catalog_product_entity_int'; + $hasVisibility = DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->where('entity_id', $productId) + ->where('attribute_id', $visibilityAttributeId) + ->where('store_id', 0) + ->exists(); + + if (!$hasVisibility) { + DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->insert([ + 'attribute_id' => $visibilityAttributeId, + 'store_id' => 0, + 'entity_id' => $productId, + 'value' => 4, // Catalog, Search + ]); + } else { + // Update to Catalog, Search if it's Not Visible or Search Only + $currentVisibility = DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->where('entity_id', $productId) + ->where('attribute_id', $visibilityAttributeId) + ->where('store_id', 0) + ->value('value'); + + // Only update if visibility is 1 (Not Visible) or 2 (Catalog) + // 3 = Search, 4 = Catalog, Search (both) + if ($currentVisibility == 1 || $currentVisibility == 2) { + DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->where('entity_id', $productId) + ->where('attribute_id', $visibilityAttributeId) + ->where('store_id', 0) + ->update(['value' => 4]); + } + } + } + } catch (Exception $e) { + Log::warning("Error ensuring product {$productId} is enabled and visible: " . $e->getMessage()); + } + } + + /** + * Get Magento 1 category tree with products + */ + public function getMagento1CategoryTreeWithProducts() + { + try { + $categories = $this->getMagento1Categories(); + $categoryMap = []; + + // Build category map + foreach ($categories as $category) { + $categoryMap[$category->entity_id] = [ + 'id' => $category->entity_id, + 'name' => $category->name, + 'parent_id' => $category->parent_id, + 'is_active' => $category->is_active ?? true, + 'path' => $category->path ?? '', + 'children' => [], + 'products' => [] + ]; + } + + // Get all products in categories + $categoryProducts = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_category_product') + ->select('category_id', 'product_id', 'position') + ->orderBy('category_id') + ->orderBy('position') + ->get(); + + // Get product details + $productIds = $categoryProducts->pluck('product_id')->unique()->toArray(); + $products = []; + + if (!empty($productIds)) { + // Check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + $hasSkuColumn = false; + } + + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if ($entityTypeId) { + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKUs - from entity table or EAV + $skus = []; + if ($hasSkuColumn) { + // SKU is stored in entity table + $skuValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->whereIn('entity_id', $productIds) + ->pluck('sku', 'entity_id') + ->toArray(); + $skus = $skuValues; + } else if ($skuAttributeId) { + // SKU is stored in EAV table + $skuValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + // Get names + $names = []; + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Build products array + foreach ($productIds as $productId) { + $sku = $skus[$productId] ?? 'N/A'; + $products[$productId] = [ + 'id' => $productId, + 'sku' => !empty($sku) ? $sku : 'N/A', + 'name' => $names[$productId] ?? 'Unnamed Product' + ]; + } + } + } + + // Assign products to categories + foreach ($categoryProducts as $cp) { + if (isset($categoryMap[$cp->category_id]) && isset($products[$cp->product_id])) { + $categoryMap[$cp->category_id]['products'][] = [ + 'id' => $products[$cp->product_id]['id'], + 'sku' => $products[$cp->product_id]['sku'], + 'name' => $products[$cp->product_id]['name'], + 'position' => $cp->position ?? 0 + ]; + } + } + + // Build tree structure + $tree = []; + foreach ($categoryMap as $categoryId => $category) { + if ($category['parent_id'] == 0 || $category['parent_id'] == 1) { + // Root category + $tree[] = $this->buildCategoryTreeWithProducts($category, $categoryMap); + } + } + + return $tree; + } catch (Exception $e) { + Log::error('Error fetching M1 category tree with products: ' . $e->getMessage()); + return []; + } + } + + /** + * Get Magento 2 category tree with products + */ + public function getMagento2CategoryTreeWithProducts() + { + try { + $categories = $this->getMagento2Categories(); + $categoryMap = []; + + // Build category map + foreach ($categories as $category) { + $categoryMap[$category->entity_id] = [ + 'id' => $category->entity_id, + 'name' => $category->name, + 'parent_id' => $category->parent_id, + 'is_active' => $category->is_active ?? true, + 'path' => $category->path ?? '', + 'children' => [], + 'products' => [] + ]; + } + + // Get all products in categories + $categoryProducts = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->select('category_id', 'product_id', 'position') + ->orderBy('category_id') + ->orderBy('position') + ->get(); + + // Get product details + $productIds = $categoryProducts->pluck('product_id')->unique()->toArray(); + $products = []; + + if (!empty($productIds)) { + // Check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + $hasSkuColumn = false; + } + + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if ($entityTypeId) { + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKUs - from entity table or EAV + $skus = []; + if ($hasSkuColumn) { + // SKU is stored in entity table + $skuValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->whereIn('entity_id', $productIds) + ->pluck('sku', 'entity_id') + ->toArray(); + $skus = $skuValues; + } else if ($skuAttributeId) { + // SKU is stored in EAV table + $skuValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + // Get names + $names = []; + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Build products array + foreach ($productIds as $productId) { + $sku = $skus[$productId] ?? 'N/A'; + $products[$productId] = [ + 'id' => $productId, + 'sku' => !empty($sku) ? $sku : 'N/A', + 'name' => $names[$productId] ?? 'Unnamed Product' + ]; + } + } + } + + // Assign products to categories + foreach ($categoryProducts as $cp) { + if (isset($categoryMap[$cp->category_id]) && isset($products[$cp->product_id])) { + $categoryMap[$cp->category_id]['products'][] = [ + 'id' => $products[$cp->product_id]['id'], + 'sku' => $products[$cp->product_id]['sku'], + 'name' => $products[$cp->product_id]['name'], + 'position' => $cp->position ?? 0 + ]; + } + } + + // Build tree structure + $tree = []; + foreach ($categoryMap as $categoryId => $category) { + if ($category['parent_id'] == 0 || $category['parent_id'] == 1) { + // Root category + $tree[] = $this->buildCategoryTreeWithProducts($category, $categoryMap); + } + } + + return $tree; + } catch (Exception $e) { + Log::error('Error fetching M2 category tree with products: ' . $e->getMessage()); + return []; + } + } + + /** + * Build category tree structure with products recursively + */ + protected function buildCategoryTreeWithProducts($category, $categoryMap) + { + $node = [ + 'id' => $category['id'], + 'name' => $category['name'], + 'is_active' => $category['is_active'], + 'products' => $category['products'], + 'children' => [] + ]; + + // Find children + foreach ($categoryMap as $catId => $cat) { + if ($cat['parent_id'] == $category['id']) { + $node['children'][] = $this->buildCategoryTreeWithProducts($cat, $categoryMap); + } + } + + return $node; + } + + /** + * Get category name by ID + */ + protected function getCategoryNameById($categoryId, $source = 'm1') + { + try { + $connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection; + $prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix; + + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_category') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return null; + } + + $nameAttributeId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + if (!$nameAttributeId) { + return null; + } + + $name = DB::connection($connection) + ->table($prefix . 'catalog_category_entity_varchar') + ->where('entity_id', $categoryId) + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->value('value'); + + return $name; + } catch (Exception $e) { + Log::warning("Error getting category name for ID {$categoryId}: " . $e->getMessage()); + return null; + } + } + + /** + * Find category by name (case-insensitive, matches any parent) + */ + protected function findCategoryByName($categoryName, $source = 'm2') + { + try { + $connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection; + $prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix; + + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_category') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return null; + } + + $nameAttributeId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + if (!$nameAttributeId) { + return null; + } + + // Find category by name (case-insensitive) + $category = DB::connection($connection) + ->table($prefix . 'catalog_category_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->whereRaw('LOWER(value) = ?', [strtolower(trim($categoryName))]) + ->first(); + + return $category ? $category->entity_id : null; + } catch (Exception $e) { + Log::warning("Error finding category by name '{$categoryName}': " . $e->getMessage()); + return null; + } + } + + /** + * Get category mapping from previous migrations + */ +protected function getCategoryMapping() + { + // Try to get mapping from category_mapping table or build it from existing categories + // Build it by matching category names (case-insensitive, ignoring parent differences) + $mapping = []; + + try { + $m1Categories = $this->getMagento1Categories(); + $m2Categories = $this->getMagento2Categories(); + + // Create a map of M2 categories by normalized name (case-insensitive) + // Use the first match if multiple categories have the same name + $m2CategoryMap = []; + foreach ($m2Categories as $m2Cat) { + $normalizedName = strtolower(trim($m2Cat->name ?? '')); + if (!empty($normalizedName) && !isset($m2CategoryMap[$normalizedName])) { + $m2CategoryMap[$normalizedName] = $m2Cat->entity_id; + } + } + + // Match M1 categories to M2 by normalized name + foreach ($m1Categories as $m1Cat) { + $normalizedName = strtolower(trim($m1Cat->name ?? '')); + if (!empty($normalizedName) && isset($m2CategoryMap[$normalizedName])) { + $mapping[$m1Cat->entity_id] = $m2CategoryMap[$normalizedName]; + } + } + } catch (Exception $e) { + Log::warning('Error building category mapping: ' . $e->getMessage()); + } + + return $mapping; + } + /** * Recursively delete all children of a category */ diff --git a/resources/views/migration/index.blade.php b/resources/views/migration/index.blade.php index ac8434f..4c30ba1 100644 --- a/resources/views/migration/index.blade.php +++ b/resources/views/migration/index.blade.php @@ -484,6 +484,127 @@ .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; + max-height: calc(100vh - 40px); + overflow-y: auto; + z-index: 10000; + border: 1px solid #e0e0e0; + animation: popupFadeIn 0.2s ease-out; + } + + .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; + } @@ -500,6 +621,7 @@ + @@ -1001,6 +1123,322 @@ class="btn btn-primary" + + +
+ +
+

🚀 Product Migration

+
+

What happens when you click "Start Product Migration"?

+

The product migration process will:

+
    +
  • Create new products: If a product with the same SKU doesn't exist in Magento 2, it will be created with all its attributes (name, description, price, weight, status, etc.)
  • +
  • Update existing products: If a product with the same SKU already exists in Magento 2, it will be updated with the latest data from Magento 1
  • +
  • Migrate product attributes: All product attributes including SKU, name, description, price, weight, status, visibility, and tax class will be migrated
  • +
  • Assign to categories: Products will be assigned to the corresponding categories in Magento 2 based on the category mapping from previous migrations
  • +
  • Preserve product types: Product types (simple, configurable, etc.) will be maintained
  • +
  • Generate logs: Detailed migration logs showing which products were added, updated, or encountered errors
  • +
+

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

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

📋 Product Migration Logs

+

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

+
+
+
+ No product migration logs yet. Click "Run Dry Run" or "Start Product Migration" to begin. +
+
+
+
+ + +
+

🌳 Category Tree with Products

+

View all categories with their associated products in a tree structure.

+ +
+ +
+

Magento 1 Categories

+
+
Click "Load M1 Category Tree" to view categories and their products.
+
+
+ +
+
+ + +
+

Magento 2 Categories

+
+
Click "Load M2 Category Tree" to view categories and their products.
+
+
+ +
+
+
+
+ + +
+

🔄 Sync Product Categories

+
+

This tool will update Magento 2 products to be in the same categories as their Magento 1 counterparts.

+
    +
  • Matches products between M1 and M2 by SKU or Product ID
  • +
  • Uses the category mapping from previous migrations
  • +
  • Adds missing category associations
  • +
  • Removes category associations that don't exist in M1
  • +
  • Preserves product positions in categories
  • +
+

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

+
+ +
+ +
+ + +
+

Sync Logs

+ +
+
+ + +
+

⚠️ Magento 1 Products Not Found in Magento 2

+

These products exist in Magento 1 but do not have a matching SKU or ID in Magento 2:

+ @if($m1ProductsNotInM2->count() > 0) +
+ + + + + + + + + + + + @foreach($m1ProductsNotInM2 as $product) + + + + + + + + @endforeach + +
IDSKUNameTypeCreated
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} + @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif +
+
+
+ Total: {{ $m1ProductsNotInM2->count() }} {{ Str::plural('product', $m1ProductsNotInM2->count()) }} found in Magento 1 but not in Magento 2. +
+ @else +
+ ✓ All Magento 1 products have matching SKUs or IDs in Magento 2. +
+ @endif +
+ + +
+

⚠️ Magento 2 Products Not Found in Magento 1

+

These products exist in Magento 2 but do not have a matching SKU or ID in Magento 1:

+ @if($m2ProductsNotInM1->count() > 0) +
+ + + + + + + + + + + + + @foreach($m2ProductsNotInM1 as $product) + + + + + + + + + @endforeach + +
IDSKUNameTypeCreatedActions
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} + @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif + + +
+
+
+ Total: {{ $m2ProductsNotInM1->count() }} {{ Str::plural('product', $m2ProductsNotInM1->count()) }} found in Magento 2 but not in Magento 1. +
+ @else +
+ ✓ All Magento 2 products have matching SKUs or IDs in Magento 1. +
+ @endif +
+ + +
+

📦 Products

+

View all products from Magento 1 and Magento 2

+ +
+ +
+

Magento 1 Products ({{ $m1Products->count() }})

+
+ @if($m1Products->count() > 0) + + + + + + + + + + + + @foreach($m1Products as $product) + + + + + + + + @endforeach + +
IDSKUNameTypeCreated
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} + @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif +
+ @else +
+ No products found. Please check your database connection. +
+ @endif +
+
+ + +
+

Magento 2 Products ({{ $m2Products->count() }})

+
+ @if($m2Products->count() > 0) + + + + + + + + + + + + @foreach($m2Products as $product) + + + + + + + + @endforeach + +
IDSKUNameTypeCreated
{{ $product->entity_id }}{{ $product->sku ?? 'N/A' }}{{ $product->name ?? 'Unnamed Product' }}{{ $product->type_id ?? 'N/A' }} + @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif +
+ @else +
+ No products found. Please check your database connection. +
+ @endif +
+
+
+
+
@@ -1036,6 +1474,446 @@ function previewCategories() { } } + function loadM1CategoryTreeWithProducts() { + const container = document.getElementById('m1-category-products-tree-container'); + container.innerHTML = '
Loading categories and products...
'; + + fetch('{{ route("migration.magento1-category-tree-with-products") }}') + .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 = '
No categories found
'; + } + } else { + container.innerHTML = `
Error: ${data.message || 'Failed to load categories'}
`; + } + }) + .catch(error => { + container.innerHTML = `
Error: ${error.message}
`; + }); + } + + function loadM2CategoryTreeWithProducts() { + const container = document.getElementById('m2-category-products-tree-container'); + container.innerHTML = '
Loading categories and products...
'; + + fetch('{{ route("migration.magento2-category-tree-with-products") }}') + .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 = '
No categories found
'; + } + } else { + container.innerHTML = `
Error: ${data.message || 'Failed to load categories'}
`; + } + }) + .catch(error => { + container.innerHTML = `
Error: ${error.message}
`; + }); + } + + function renderCategoryTreeWithProducts(container, tree) { + tree.forEach(node => { + const nodeElement = createCategoryWithProductsNode(node); + container.appendChild(nodeElement); + }); + } + + function createCategoryWithProductsNode(node) { + const nodeDiv = document.createElement('div'); + nodeDiv.className = 'tree-node'; + nodeDiv.setAttribute('data-category-id', node.id); + + const hasChildren = node.children && node.children.length > 0; + const hasProducts = 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'; + const productCount = node.products ? node.products.length : 0; + labelText.textContent = `[${node.id}] ${node.name || 'Unnamed Category'} (${productCount} product${productCount !== 1 ? 's' : ''})`; + + 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); + + // Products section + if (hasProducts) { + const productsDiv = document.createElement('div'); + productsDiv.className = 'tree-children tree-products'; + productsDiv.style.display = 'none'; + + const productsHeader = document.createElement('div'); + productsHeader.style.cssText = 'padding: 8px 12px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; font-weight: 600; color: #333; font-size: 0.9em;'; + productsHeader.textContent = `Products (${node.products.length}):`; + productsDiv.appendChild(productsHeader); + + const productsList = document.createElement('div'); + productsList.style.cssText = 'padding: 8px;'; + + // Sort products by position + const sortedProducts = [...node.products].sort((a, b) => (a.position || 0) - (b.position || 0)); + + sortedProducts.forEach(product => { + const productDiv = document.createElement('div'); + productDiv.style.cssText = 'padding: 6px 12px; margin: 4px 0; background: white; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 0.85em;'; + productDiv.innerHTML = ` + ID: ${product.id} | + SKU: ${product.sku || 'N/A'} | + Name: ${product.name || 'Unnamed Product'} + `; + productsList.appendChild(productDiv); + }); + + productsDiv.appendChild(productsList); + nodeDiv.appendChild(productsDiv); + } + + // Children categories + 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; + } + + function syncProductCategories() { + if (!confirm('Are you sure you want to sync product categories from Magento 1 to Magento 2?\n\nThis will update category assignments for all matching products. Make sure you have a backup!')) { + return; + } + + const button = document.getElementById('syncProductCategoriesBtn'); + const logsDiv = document.getElementById('syncProductCategoriesLogs'); + const originalText = button.textContent; + + button.disabled = true; + button.textContent = 'Syncing Categories...'; + button.style.background = '#999'; + button.style.cursor = 'not-allowed'; + + logsDiv.style.display = 'block'; + logsDiv.innerHTML = '
Starting category sync...
'; + + fetch('/migration/products/sync-categories', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + } + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + button.textContent = originalText; + button.style.background = '#1976D2'; + button.style.cursor = 'pointer'; + + if (data.success) { + // Display logs + let logHtml = ''; + if (data.log && data.log.length > 0) { + data.log.forEach(logEntry => { + let logColor = '#333'; + if (logEntry.includes('ERROR')) { + logColor = '#d32f2f'; + } else if (logEntry.includes('UPDATED')) { + logColor = '#1976D2'; + } else if (logEntry.includes('SKIPPED')) { + logColor = '#666'; + } + logHtml += `
${logEntry}
`; + }); + } else { + logHtml = '
No log entries available.
'; + } + + logHtml += `
✓ ${data.message}
`; + logsDiv.innerHTML = logHtml; + + // Scroll to bottom of logs + logsDiv.scrollTop = logsDiv.scrollHeight; + } else { + logsDiv.innerHTML = `
✗ Error: ${data.message || 'Failed to sync product categories'}
`; + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + button.style.background = '#1976D2'; + button.style.cursor = 'pointer'; + + logsDiv.innerHTML = `
✗ Error: ${error.message}
`; + }); + } + + function deleteM2Product(productId, productName, productSku) { + if (!confirm(`Are you sure you want to delete product ID ${productId}?\n\nProduct: ${productName}\nSKU: ${productSku}\n\nThis action cannot be undone. Make sure you have a backup!`)) { + return; + } + + const row = document.getElementById(`product-row-${productId}`); + if (!row) return; + + // Disable the button + const button = row.querySelector('button'); + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Deleting...'; + button.style.background = '#999'; + button.style.cursor = 'not-allowed'; + + fetch(`/migration/products/${productId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Remove the row from the table + row.style.opacity = '0.5'; + row.style.transition = 'opacity 0.3s'; + setTimeout(() => { + const section = row.closest('.section'); + row.remove(); + + // Update the count + const table = section.querySelector('table'); + const remainingRows = table ? table.querySelectorAll('tbody tr').length : 0; + const summaryDiv = section.querySelector('div[style*="background: #fff3cd"]'); + + if (remainingRows > 0) { + if (summaryDiv) { + summaryDiv.innerHTML = `Total: ${remainingRows} ${remainingRows === 1 ? 'product' : 'products'} found in Magento 2 but not in Magento 1.`; + } + } else { + // Show success message if all products are deleted + const tableContainer = section.querySelector('div[style*="max-height: 400px"]'); + if (tableContainer) tableContainer.style.display = 'none'; + if (summaryDiv) summaryDiv.style.display = 'none'; + + // Remove existing success message if any + const existingSuccess = section.querySelector('div[style*="background: #d4edda"]'); + if (existingSuccess) existingSuccess.remove(); + + const successDiv = document.createElement('div'); + successDiv.style.cssText = 'margin-top: 15px; padding: 15px; background: #d4edda; border-left: 4px solid #28a745; border-radius: 4px; color: #155724;'; + successDiv.textContent = '✓ All Magento 2 products have matching SKUs or IDs in Magento 1.'; + section.appendChild(successDiv); + } + }, 300); + } else { + button.disabled = false; + button.textContent = originalText; + button.style.background = '#d32f2f'; + button.style.cursor = 'pointer'; + alert('✗ Error: ' + (data.message || 'Failed to delete product')); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + button.style.background = '#d32f2f'; + button.style.cursor = 'pointer'; + alert('✗ Error: ' + error.message); + }); + } + + function deleteProductsAboveM1Max() { + if (!confirm('Are you sure you want to delete all products in Magento 2 that have entity_id greater than the maximum product ID in Magento 1?\n\nThis will permanently delete these products and cannot be undone. Make sure you have a backup!')) { + return; + } + + const button = document.getElementById('deleteProductsAboveM1MaxBtn'); + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Deleting Products...'; + button.style.background = '#999'; + button.style.cursor = 'not-allowed'; + + fetch('{{ route("migration.delete-products-above-m1-max") }}', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + } + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + button.textContent = originalText; + button.style.background = '#d32f2f'; + button.style.cursor = 'pointer'; + + if (data.success) { + alert(`✓ ${data.message}\n\nDeleted ${data.deleted} product(s). Max M1 Product ID: ${data.max_m1_id}`); + // Optionally reload the page to refresh product lists + // window.location.reload(); + } else { + alert('✗ Error: ' + (data.message || 'Failed to delete products')); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + button.style.background = '#d32f2f'; + button.style.cursor = 'pointer'; + + alert('✗ Error: ' + error.message); + }); + } + + function startProductMigration(dryRun = false) { + if (!dryRun) { + if (!confirm('Are you sure you want to migrate all products from Magento 1 to Magento 2?\n\nThis will create new products or update existing ones based on SKU. Make sure you have a backup!')) { + return; + } + } + + 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 Products...'; + button.style.background = '#999'; + button.style.cursor = 'not-allowed'; + + // Clear previous logs and hide missing attributes section + const logContent = document.getElementById('productMigrationLogContent'); + logContent.innerHTML = '
' + (dryRun ? 'Starting dry run...' : 'Starting product migration...') + '
'; + const missingSection = document.getElementById('missingAttributesSection'); + missingSection.style.display = 'none'; + const missingTableBody = document.getElementById('missingAttributesTableBody'); + missingTableBody.innerHTML = ''; + + fetch('{{ route("migration.migrate-products") }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ dry_run: dryRun }) + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + otherButton.disabled = false; + button.textContent = originalText; + button.style.background = dryRun ? '#6c757d' : '#1976D2'; + button.style.cursor = 'pointer'; + + if (data.success) { + // Clear and add new logs + logContent.innerHTML = ''; + + // Add summary + const summary = document.createElement('div'); + summary.className = 'log-entry'; + const summaryColor = dryRun ? '#ffc107' : '#28a745'; + const summaryBg = dryRun ? '#fff3cd' : '#d4edda'; + summary.style.cssText = `color: ${summaryColor}; font-weight: 600; margin-bottom: 10px; padding: 10px; background: ${summaryBg}; border-left: 4px solid ${summaryColor}; border-radius: 4px;`; + summary.textContent = `${dryRun ? '✓ Dry run completed!' : '✓ Migration completed!'} Added: ${data.added}, Updated: ${data.updated}, Errors: ${data.errors}`; + logContent.appendChild(summary); + + // Show missing attributes if any + if (data.missing_attributes && data.missing_attributes.length > 0) { + missingSection.style.display = 'block'; + data.missing_attributes.forEach(attr => { + const row = document.createElement('tr'); + row.style.cssText = 'border-bottom: 1px solid #e0e0e0;'; + row.innerHTML = ` + ${attr.code} + ${attr.label || 'N/A'} + ${attr.type || 'varchar'} + `; + missingTableBody.appendChild(row); + }); + } + + // Add detailed logs + if (data.log && data.log.length > 0) { + data.log.forEach(logEntry => { + const entry = document.createElement('div'); + entry.className = 'log-entry'; + + if (logEntry.includes('ERROR')) { + entry.style.cssText = 'color: #d32f2f; margin: 5px 0; padding: 5px;'; + } else if (logEntry.includes('Created new product') || logEntry.includes('Added new') || logEntry.includes('Would create')) { + entry.style.cssText = 'color: #28a745; margin: 5px 0; padding: 5px;'; + } else if (logEntry.includes('Updating existing') || logEntry.includes('Updated') || logEntry.includes('Would update')) { + entry.style.cssText = 'color: #1976D2; margin: 5px 0; padding: 5px;'; + } else { + entry.style.cssText = 'color: #666; margin: 5px 0; padding: 5px;'; + } + + entry.textContent = logEntry; + logContent.appendChild(entry); + }); + } + + // Scroll to bottom of log container + const logContainer = document.getElementById('productMigrationLogContainer'); + logContainer.scrollTop = logContainer.scrollHeight; + } else { + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry'; + errorEntry.style.cssText = 'color: #d32f2f; font-weight: 600; margin: 5px 0; padding: 5px;'; + 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.background = dryRun ? '#6c757d' : '#1976D2'; + button.style.cursor = 'pointer'; + + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry'; + errorEntry.style.cssText = 'color: #d32f2f; font-weight: 600; margin: 5px 0; padding: 5px;'; + errorEntry.textContent = '✗ Error: ' + error.message; + logContent.appendChild(errorEntry); + }); + } + function startMigration() { const storeMapping = {}; const selects = document.querySelectorAll('select[name^="store_mapping"]'); @@ -1231,6 +2109,95 @@ function loadM2Tree() { }); } + // Show delete confirmation popup + function showDeleteConfirmPopup(buttonElement, categoryId, categoryName, source, hasChildren, childrenCount) { + // Remove any existing popup + const existingPopup = document.querySelector('.delete-confirm-popup'); + const existingOverlay = document.querySelector('.popup-overlay'); + if (existingPopup) existingPopup.remove(); + if (existingOverlay) existingOverlay.remove(); + + // Create overlay + const overlay = document.createElement('div'); + overlay.className = 'popup-overlay'; + overlay.onclick = function() { + closeDeleteConfirmPopup(); + }; + document.body.appendChild(overlay); + + // Create popup + const popup = document.createElement('div'); + popup.className = 'delete-confirm-popup'; + + // Create popup content + 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 "${categoryName}" and all ${childrenCount} subcategory(ies)? This action cannot be undone.`; + } else { + message.innerHTML = `Are you sure you want to delete the category "${categoryName}"? 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); + + // Hide arrow when centered + popup.classList.add('no-arrow'); + + // Add to DOM + document.body.appendChild(popup); + + // Center popup in viewport using CSS transform (works immediately) + popup.style.position = 'fixed'; + popup.style.top = '50%'; + popup.style.left = '50%'; + popup.style.transform = 'translate(-50%, -50%)'; + popup.style.zIndex = '10000'; + + // Close on Escape key + const escapeHandler = function(e) { + if (e.key === 'Escape') { + closeDeleteConfirmPopup(); + document.removeEventListener('keydown', escapeHandler); + } + }; + document.addEventListener('keydown', escapeHandler); + } + + // Close delete confirmation popup + function closeDeleteConfirmPopup() { + const popup = document.querySelector('.delete-confirm-popup'); + const overlay = document.querySelector('.popup-overlay'); + if (popup) popup.remove(); + if (overlay) overlay.remove(); + } + // Delete category function deleteCategory(categoryId, categoryName, source) { const container = source === 'm1' ? document.getElementById('m1-tree-container') : document.getElementById('m2-tree-container'); @@ -1256,7 +2223,6 @@ function deleteCategory(categoryId, categoryName, source) { } else { loadM2Tree(); } - alert('Category deleted successfully!'); } else { container.innerHTML = originalContent; alert('Error: ' + (data.message || 'Failed to delete category')); @@ -1458,15 +2424,10 @@ function createTreeNode(node, source = 'm2') { const deleteBtn = document.createElement('button'); deleteBtn.className = 'tree-delete-btn'; deleteBtn.textContent = 'Delete'; - const warningText = hasChildren - ? `Delete this category and all ${node.children.length} subcategory(ies)?\n\nThis action cannot be undone.` - : `Are you sure you want to delete the category "${node.name}"?\n\nThis action cannot be undone.`; deleteBtn.title = hasChildren ? 'Delete this category and all subcategories' : 'Delete this category'; deleteBtn.onclick = function(e) { e.stopPropagation(); - if (confirm(warningText)) { - deleteCategory(node.id, node.name, source); - } + showDeleteConfirmPopup(deleteBtn, node.id, node.name, source, hasChildren, node.children ? node.children.length : 0); }; label.appendChild(deleteBtn); } @@ -1492,16 +2453,29 @@ function createTreeNode(node, source = 'm2') { function toggleNode(toggleElement) { const nodeItem = toggleElement.parentElement; const nodeDiv = nodeItem.parentElement; - const childrenDiv = nodeDiv.querySelector('.tree-children'); + const childrenDivs = nodeDiv.querySelectorAll('.tree-children'); + + if (childrenDivs.length > 0) { + // Check if any child div is expanded + let isExpanded = false; + childrenDivs.forEach(div => { + if (div.style.display !== 'none') { + isExpanded = true; + } + }); - if (childrenDiv) { - const isExpanded = childrenDiv.classList.contains('expanded'); if (isExpanded) { - childrenDiv.classList.remove('expanded'); + // Collapse all children + childrenDivs.forEach(div => { + div.style.display = 'none'; + }); toggleElement.classList.remove('expanded'); toggleElement.classList.add('collapsed'); } else { - childrenDiv.classList.add('expanded'); + // Expand all children + childrenDivs.forEach(div => { + div.style.display = 'block'; + }); toggleElement.classList.remove('collapsed'); toggleElement.classList.add('expanded'); } diff --git a/routes/web.php b/routes/web.php index 4230653..ac748f3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,9 +13,15 @@ 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::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'); });