From 659b3705fc927263d57a3a0a744451dddf578801 Mon Sep 17 00:00:00 2001 From: Chris Rosenau Date: Wed, 19 Nov 2025 19:07:08 -0700 Subject: [PATCH] updated more items --- app/Http/Controllers/CustomersController.php | 81 ++ app/Http/Controllers/ProductsController.php | 24 + .../MagentoCategoryMigrationService.php | 1073 ++++++++++++++++- resources/js/customers.js | 115 ++ resources/js/products.js | 79 ++ resources/views/customers/index.blade.php | 161 +++ resources/views/partials/navigation.blade.php | 3 + resources/views/products/index.blade.php | 25 + routes/web.php | 9 + vite.config.js | 1 + 10 files changed, 1569 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/CustomersController.php create mode 100644 resources/js/customers.js create mode 100644 resources/views/customers/index.blade.php diff --git a/app/Http/Controllers/CustomersController.php b/app/Http/Controllers/CustomersController.php new file mode 100644 index 0000000..b5587db --- /dev/null +++ b/app/Http/Controllers/CustomersController.php @@ -0,0 +1,81 @@ +migrationService = $migrationService; + } + + /** + * Show the customers page + */ + public function index() + { + $m1Customers = $this->migrationService->getMagento1Customers(); + $m2Customers = $this->migrationService->getMagento2Customers(); + $m1CustomersNotInM2 = $this->migrationService->getM1CustomersNotInM2(); + $m2CustomersNotInM1 = $this->migrationService->getM2CustomersNotInM1(); + + return view('customers.index', [ + 'm1Customers' => $m1Customers, + 'm2Customers' => $m2Customers, + 'm1CustomersNotInM2' => $m1CustomersNotInM2, + 'm2CustomersNotInM1' => $m2CustomersNotInM1, + ]); + } + + /** + * Migrate all customers from Magento 1 to Magento 2 + */ + public function migrateCustomers(Request $request) + { + try { + $dryRun = $request->input('dry_run', false); + $result = $this->migrationService->migrateCustomers($dryRun); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Customer migration error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [] + ], 500); + } + } + + /** + * Delete a single customer from Magento 2 + */ + public function deleteM2Customer(Request $request, $customerId) + { + try { + $result = $this->migrationService->deleteM2Customer($customerId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Delete M2 customer error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage() + ], 500); + } + } +} + diff --git a/app/Http/Controllers/ProductsController.php b/app/Http/Controllers/ProductsController.php index 7ec6a73..5604289 100644 --- a/app/Http/Controllers/ProductsController.php +++ b/app/Http/Controllers/ProductsController.php @@ -123,5 +123,29 @@ public function deleteM2ProductsAboveM1Max(Request $request) ], 500); } } + + /** + * Fix category products - ensure products are added to categories if missing from catalog_category_product table + */ + public function fixCategoryProducts(Request $request) + { + try { + $result = $this->migrationService->fixCategoryProducts(); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Fix category products error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Fix failed: ' . $e->getMessage(), + 'added' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ], 500); + } + } } diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index e1468ed..bd4f4a1 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -2174,8 +2174,9 @@ public function migrateProducts($dryRun = false) } // 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); + // Migrate ALL data from ALL catalog_product_entity_* tables + // This includes images, all store views, and all attributes + $missingAttrs = $this->migrateAllProductEntityData($m1Product->entity_id, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $allM1AttributeIds, $allM2AttributeIds, $dryRun, $allM1Attributes); if ($missingAttrs) { foreach ($missingAttrs as $attr) { // Check if this attribute is already in the list @@ -2192,6 +2193,11 @@ public function migrateProducts($dryRun = false) } } + // Migrate media gallery images (skip in dry run) + if (!$dryRun) { + $this->migrateProductMediaGallery($m1Product->entity_id, $m2ProductId, $m1EntityTypeId); + } + // Migrate category associations (skip in dry run) if (!$dryRun) { // Ensure product is enabled and visible (required for products to show in categories after reindex) @@ -2378,6 +2384,375 @@ protected function getMagento2ProductAttributeIds() return $attributeIds; } + /** + * Migrate ALL data from ALL catalog_product_entity_* tables for a product + * This ensures we migrate images, all store views, and all attributes + */ + protected function migrateAllProductEntityData($m1ProductId, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $m1AttributeIds, $m2AttributeIds, $dryRun = false, $allM1Attributes = null) + { + $missingAttributes = []; + + // All possible backend types for EAV attributes + $backendTypes = ['varchar', 'int', 'text', 'decimal', 'datetime']; + + // Get attribute mapping: M1 attribute_id => M2 attribute_id + $attributeMapping = []; + if ($allM1Attributes) { + foreach ($allM1Attributes as $m1Attr) { + $attrCode = $m1Attr->attribute_code; + if (isset($m2AttributeIds[$attrCode])) { + $attributeMapping[$m1Attr->attribute_id] = [ + 'm2_attr_id' => $m2AttributeIds[$attrCode], + 'backend_type' => $m1Attr->backend_type ?? 'varchar', + 'attribute_code' => $attrCode + ]; + } + } + } + + // Migrate data from each backend type table + foreach ($backendTypes as $backendType) { + $m1Table = $this->magento1Prefix . 'catalog_product_entity_' . $backendType; + $m2Table = $this->magento2Prefix . 'catalog_product_entity_' . $backendType; + + try { + // Check if M1 table exists + $m1Rows = DB::connection($this->magento1Connection) + ->table($m1Table) + ->where('entity_id', $m1ProductId) + ->get(); + + if ($m1Rows->isEmpty()) { + continue; // No data in this table for this product + } + + // Check if M2 table exists + try { + DB::connection($this->magento2Connection) + ->table($m2Table) + ->limit(1) + ->first(); + } catch (Exception $e) { + Log::warning("M2 table {$m2Table} doesn't exist: " . $e->getMessage()); + continue; + } + + if (!$dryRun) { + // Migrate each row + foreach ($m1Rows as $m1Row) { + $m1AttrId = $m1Row->attribute_id; + + // Check if we have a mapping for this attribute + if (!isset($attributeMapping[$m1AttrId])) { + // Attribute doesn't exist in M2, log it + if ($allM1Attributes) { + $attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId); + if ($attr) { + $missingAttr = [ + 'code' => $attr->attribute_code ?? 'unknown', + 'label' => $attr->frontend_label ?? 'Unknown', + 'type' => $attr->backend_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; + } + } + } + continue; // Skip this attribute + } + + $m2AttrId = $attributeMapping[$m1AttrId]['m2_attr_id']; + $storeId = $m1Row->store_id ?? 0; + + // Check if this row already exists in M2 + $exists = DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $m2AttrId) + ->where('store_id', $storeId) + ->exists(); + + if ($exists) { + // Update existing row + DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $m2AttrId) + ->where('store_id', $storeId) + ->update(['value' => $m1Row->value]); + } else { + // Insert new row + DB::connection($this->magento2Connection) + ->table($m2Table) + ->insert([ + 'attribute_id' => $m2AttrId, + 'store_id' => $storeId, + 'entity_id' => $m2ProductId, + 'value' => $m1Row->value, + ]); + } + } + } + } catch (Exception $e) { + // Table doesn't exist in M1, skip it + Log::debug("M1 table {$m1Table} doesn't exist or error: " . $e->getMessage()); + continue; + } + } + + return $missingAttributes; + } + + /** + * Migrate product media gallery images from M1 to M2 + * Populates catalog_product_entity_media_gallery, catalog_product_entity_media_gallery_value, + * and catalog_product_entity_media_gallery_value_to_entity tables + */ + protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1EntityTypeId) + { + try { + // Get media_gallery attribute ID from M1 + $m1MediaGalleryAttrId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $m1EntityTypeId) + ->where('attribute_code', 'media_gallery') + ->value('attribute_id'); + + if (!$m1MediaGalleryAttrId) { + // Media gallery attribute doesn't exist in M1, skip + return; + } + + // Get media_gallery attribute ID from M2 + $m2EntityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$m2EntityTypeId) { + return; + } + + $m2MediaGalleryAttrId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $m2EntityTypeId) + ->where('attribute_code', 'media_gallery') + ->value('attribute_id'); + + if (!$m2MediaGalleryAttrId) { + // Media gallery attribute doesn't exist in M2, skip + return; + } + + $images = []; + + // First, try to get images from M1 media gallery tables (if they exist) + try { + $m1MediaGalleryTable = $this->magento1Prefix . 'catalog_product_entity_media_gallery'; + $m1MediaGalleryValueTable = $this->magento1Prefix . 'catalog_product_entity_media_gallery_value'; + + // Check if M1 media gallery tables exist + $m1GalleryImages = DB::connection($this->magento1Connection) + ->table($m1MediaGalleryTable) + ->where('entity_id', $m1ProductId) + ->get(); + + if ($m1GalleryImages->isNotEmpty()) { + // M1 has media gallery tables, use them + foreach ($m1GalleryImages as $m1Image) { + $valueId = $m1Image->value_id; + + // Get value data + $m1Value = DB::connection($this->magento1Connection) + ->table($m1MediaGalleryValueTable) + ->where('value_id', $valueId) + ->where('store_id', 0) + ->first(); + + $images[] = [ + 'file' => $m1Image->value, + 'label' => $m1Value->label ?? null, + 'position' => $m1Value->position ?? 0, + 'disabled' => $m1Value->disabled ?? 0, + ]; + } + } + } catch (Exception $e) { + // M1 media gallery tables don't exist, fall back to varchar table + Log::debug("M1 media gallery tables not found, using varchar table: " . $e->getMessage()); + } + + // If no images from gallery tables, try varchar table (serialized data) + if (empty($images)) { + $m1MediaGalleryData = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('entity_id', $m1ProductId) + ->where('attribute_id', $m1MediaGalleryAttrId) + ->where('store_id', 0) + ->value('value'); + + if (empty($m1MediaGalleryData)) { + // No media gallery data in M1, skip + return; + } + + // Parse the media gallery data (Magento 1 stores it as serialized PHP) + if (is_string($m1MediaGalleryData)) { + // Try to unserialize (Magento 1 format) + $unserialized = @unserialize($m1MediaGalleryData); + if ($unserialized !== false && is_array($unserialized)) { + $parsedImages = $unserialized; + } else { + // Try JSON decode (some versions might use JSON) + $jsonDecoded = @json_decode($m1MediaGalleryData, true); + if (is_array($jsonDecoded)) { + $parsedImages = $jsonDecoded; + } else { + // If it's a simple string, treat it as a single image path + if (!empty(trim($m1MediaGalleryData))) { + $parsedImages = [['file' => trim($m1MediaGalleryData)]]; + } else { + $parsedImages = []; + } + } + } + + // Convert parsed images to our format + if (!empty($parsedImages)) { + if (isset($parsedImages['images']) && is_array($parsedImages['images'])) { + $parsedImages = $parsedImages['images']; + } + + foreach ($parsedImages as $img) { + if (is_string($img)) { + $images[] = ['file' => $img]; + } elseif (is_array($img)) { + $images[] = [ + 'file' => $img['file'] ?? $img['value'] ?? null, + 'label' => $img['label'] ?? $img['label_default'] ?? null, + 'position' => isset($img['position']) ? (int)$img['position'] : 0, + 'disabled' => isset($img['disabled']) ? (int)$img['disabled'] : 0, + ]; + } + } + } + } + } + + if (empty($images)) { + return; + } + + // Process each image + foreach ($images as $imageData) { + // Handle different data structures + $imageFile = null; + $label = null; + $position = 0; + $disabled = 0; + + if (is_string($imageData)) { + // Simple string path + $imageFile = $imageData; + } elseif (is_array($imageData)) { + // Array structure + $imageFile = $imageData['file'] ?? $imageData['value'] ?? null; + $label = $imageData['label'] ?? $imageData['label_default'] ?? null; + $position = isset($imageData['position']) ? (int)$imageData['position'] : 0; + $disabled = isset($imageData['disabled']) ? (int)$imageData['disabled'] : 0; + } + + if (empty($imageFile)) { + continue; + } + + // Clean up the image file path (remove leading slashes, etc.) + $imageFile = ltrim($imageFile, '/'); + if (empty($imageFile)) { + continue; + } + + // Check if this image already exists in M2 for this product + $existingValueId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery') + ->where('attribute_id', $m2MediaGalleryAttrId) + ->where('value', $imageFile) + ->value('value_id'); + + if (!$existingValueId) { + // Insert into catalog_product_entity_media_gallery + $valueId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery') + ->insertGetId([ + 'attribute_id' => $m2MediaGalleryAttrId, + 'value' => $imageFile, + ]); + } else { + $valueId = $existingValueId; + } + + // Check if value_to_entity link already exists + $linkExists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value_to_entity') + ->where('value_id', $valueId) + ->where('entity_id', $m2ProductId) + ->exists(); + + if (!$linkExists) { + // Insert into catalog_product_entity_media_gallery_value_to_entity + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value_to_entity') + ->insert([ + 'value_id' => $valueId, + 'entity_id' => $m2ProductId, + ]); + } + + // Insert/update catalog_product_entity_media_gallery_value for default store (store_id = 0) + $valueExists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value') + ->where('value_id', $valueId) + ->where('store_id', 0) + ->exists(); + + if (!$valueExists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value') + ->insert([ + 'value_id' => $valueId, + 'store_id' => 0, + 'label' => $label, + 'position' => $position, + 'disabled' => $disabled, + ]); + } else { + // Update existing value + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value') + ->where('value_id', $valueId) + ->where('store_id', 0) + ->update([ + 'label' => $label, + 'position' => $position, + 'disabled' => $disabled, + ]); + } + } + + } catch (Exception $e) { + // Log error but don't fail the entire migration + Log::warning("Error migrating media gallery for product M1 ID {$m1ProductId} -> M2 ID {$m2ProductId}: " . $e->getMessage()); + } + } + /** * Migrate product attributes from M1 to M2 */ @@ -3497,6 +3872,194 @@ protected function deleteCategoryChildren($categoryId, $connection, $prefix) } } + /** + * Fix category products - ensure products are added to categories if missing from catalog_category_product table + * Only looks at M2 products and their category_ids attribute + */ + public function fixCategoryProducts() + { + try { + $this->migrationLog = []; + $addedCount = 0; + $skippedCount = 0; + $errorCount = 0; + + // 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 [ + 'success' => false, + 'message' => 'Could not find catalog_product entity type in Magento 2', + 'added' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + + // Get category_ids attribute ID + $categoryIdsAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'category_ids') + ->value('attribute_id'); + + // Get all M2 products + $m2Products = $this->getMagento2Products(); + + // Get all products that are already in catalog_category_product table + $productsInCategories = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->distinct() + ->pluck('product_id') + ->toArray(); + + // Get all valid M2 category IDs + $validCategoryIds = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_entity') + ->pluck('entity_id') + ->toArray(); + + DB::connection($this->magento2Connection)->beginTransaction(); + + foreach ($m2Products as $m2Product) { + try { + $m2ProductId = $m2Product->entity_id; + $m2Sku = $m2Product->sku ?? 'N/A'; + + // Check if product is already in catalog_category_product table + if (in_array($m2ProductId, $productsInCategories)) { + $skippedCount++; + continue; + } + + // Get category_ids from product attribute + $categoryIdsString = null; + + if ($categoryIdsAttributeId) { + // Try to get from varchar table first + $categoryIdsValue = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $categoryIdsAttributeId) + ->where('store_id', 0) + ->value('value'); + + if ($categoryIdsValue) { + $categoryIdsString = $categoryIdsValue; + } else { + // Try text table + $categoryIdsValue = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_text') + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $categoryIdsAttributeId) + ->where('store_id', 0) + ->value('value'); + + if ($categoryIdsValue) { + $categoryIdsString = $categoryIdsValue; + } + } + } + + if (empty($categoryIdsString)) { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - no category_ids attribute found"; + continue; + } + + // Parse category IDs (comma-separated string) + $categoryIds = array_filter( + array_map('trim', explode(',', $categoryIdsString)), + function($id) use ($validCategoryIds) { + return !empty($id) && is_numeric($id) && in_array((int)$id, $validCategoryIds); + } + ); + + if (empty($categoryIds)) { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - no valid category IDs found in attribute"; + continue; + } + + // Ensure product is enabled and visible + $this->ensureProductIsEnabledAndVisible($m2ProductId); + + // Add product to categories + $categoriesAdded = 0; + $invalidCategories = []; + + foreach ($categoryIds as $categoryId) { + $categoryId = (int)$categoryId; + + // Check if association already exists + $exists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('category_id', $categoryId) + ->where('product_id', $m2ProductId) + ->exists(); + + if (!$exists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->insert([ + 'category_id' => $categoryId, + 'product_id' => $m2ProductId, + 'position' => 0, // Default position + ]); + $categoriesAdded++; + } + } + + if ($categoriesAdded > 0) { + $addedCount++; + $invalidMsg = !empty($invalidCategories) ? " (invalid categories: " . implode(', ', $invalidCategories) . ")" : ""; + $this->migrationLog[] = "ADDED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - added to {$categoriesAdded} category(ies){$invalidMsg}"; + } else { + $skippedCount++; + $invalidMsg = !empty($invalidCategories) ? " (invalid categories: " . implode(', ', $invalidCategories) . ")" : ""; + $this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - categories already exist or invalid{$invalidMsg}"; + } + + } catch (Exception $e) { + $errorCount++; + $m2Sku = $m2Product->sku ?? 'N/A'; + $this->migrationLog[] = "ERROR: Failed to fix categories for M2 Product ID {$m2ProductId} (SKU: {$m2Sku}): " . $e->getMessage(); + Log::error("Error fixing category products for M2 Product ID {$m2ProductId}: " . $e->getMessage()); + } + } + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => "Fixed category products. Added: {$addedCount}, Skipped: {$skippedCount}, Errors: {$errorCount}", + 'added' => $addedCount, + 'skipped' => $skippedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog + ]; + + } catch (Exception $e) { + if (isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Error fixing category products: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to fix category products: ' . $e->getMessage(), + 'added' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + } + /** * Delete category data (attributes and entity) */ @@ -3541,5 +4104,511 @@ protected function deleteCategoryData($categoryId, $connection, $prefix) ->where('entity_id', $categoryId) ->delete(); } + + /** + * Get all customers from Magento 1 + */ + public function getMagento1Customers() + { + try { + // Get entity type ID for customer + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'customer') + ->value('entity_type_id'); + + if (!$entityTypeId) { + Log::warning('Magento 1 customer entity type not found'); + return collect([]); + } + + Log::info("Magento 1 customer entity_type_id: {$entityTypeId}"); + + // Get base customer data - email is stored directly in customer_entity table + $customers = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'customer_entity') + ->select('entity_id', 'email', 'website_id', 'group_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + + Log::info("Found " . $customers->count() . " customers in Magento 1 customer_entity table"); + + // Get firstname and lastname attribute IDs (these are in EAV) + $firstnameAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'firstname') + ->value('attribute_id'); + + $lastnameAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'lastname') + ->value('attribute_id'); + + // Get firstname and lastname from EAV + $firstnames = []; + $lastnames = []; + + if ($firstnameAttributeId) { + $firstnameValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'customer_entity_varchar') + ->where('attribute_id', $firstnameAttributeId) + ->pluck('value', 'entity_id') + ->toArray(); + $firstnames = $firstnameValues; + } + + if ($lastnameAttributeId) { + $lastnameValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'customer_entity_varchar') + ->where('attribute_id', $lastnameAttributeId) + ->pluck('value', 'entity_id') + ->toArray(); + $lastnames = $lastnameValues; + } + + // Combine data - email is already in customer object from the query + foreach ($customers as $customer) { + // Email is already in customer object, just normalize it + $customer->email = !empty($customer->email) ? trim($customer->email) : null; + $customer->firstname = $firstnames[$customer->entity_id] ?? 'N/A'; + $customer->lastname = $lastnames[$customer->entity_id] ?? 'N/A'; + } + + Log::info("Returning " . $customers->count() . " Magento 1 customers"); + return $customers; + } catch (Exception $e) { + Log::error('Error fetching Magento 1 customers: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Get all customers from Magento 2 + */ + public function getMagento2Customers() + { + try { + // Get entity type ID for customer + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'customer') + ->value('entity_type_id'); + + if (!$entityTypeId) { + Log::warning('Magento 2 customer entity type not found'); + return collect([]); + } + + Log::info("Magento 2 customer entity_type_id: {$entityTypeId}"); + + // Get base customer data - email is stored directly in customer_entity table + $customers = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'customer_entity') + ->select('entity_id', 'email', 'website_id', 'group_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + + Log::info("Found " . $customers->count() . " customers in Magento 2 customer_entity table"); + + // Get firstname and lastname attribute IDs (these are in EAV) + $firstnameAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'firstname') + ->value('attribute_id'); + + $lastnameAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'lastname') + ->value('attribute_id'); + + // Get firstname and lastname from EAV + $firstnames = []; + $lastnames = []; + + if ($firstnameAttributeId) { + $firstnameValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'customer_entity_varchar') + ->where('attribute_id', $firstnameAttributeId) + ->pluck('value', 'entity_id') + ->toArray(); + $firstnames = $firstnameValues; + } + + if ($lastnameAttributeId) { + $lastnameValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'customer_entity_varchar') + ->where('attribute_id', $lastnameAttributeId) + ->pluck('value', 'entity_id') + ->toArray(); + $lastnames = $lastnameValues; + } + + // Combine data - email is already in customer object from the query + foreach ($customers as $customer) { + // Email is already in customer object, just normalize it + $customer->email = !empty($customer->email) ? trim($customer->email) : null; + $customer->firstname = $firstnames[$customer->entity_id] ?? 'N/A'; + $customer->lastname = $lastnames[$customer->entity_id] ?? 'N/A'; + } + + Log::info("Returning " . $customers->count() . " Magento 2 customers"); + return $customers; + } catch (Exception $e) { + Log::error('Error fetching Magento 2 customers: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Get Magento 1 customers that don't exist in Magento 2 + */ + public function getM1CustomersNotInM2() + { + try { + $m1Customers = $this->getMagento1Customers(); + $m2Customers = $this->getMagento2Customers(); + + Log::info("M1 Customers Not In M2: M1 count = " . $m1Customers->count() . ", M2 count = " . $m2Customers->count()); + + // Get all M2 emails - create a set for faster lookup + $m2EmailsSet = []; + foreach ($m2Customers as $m2Customer) { + $email = $m2Customer->email ?? null; + if (!empty($email) && is_string($email)) { + $normalizedEmail = strtolower(trim($email)); + if (!empty($normalizedEmail)) { + $m2EmailsSet[$normalizedEmail] = true; + } + } + } + + Log::info("M1 Customers Not In M2: M2 emails count = " . count($m2EmailsSet)); + + // Get all M1 emails for comparison + $m1EmailsWithCustomers = []; + foreach ($m1Customers as $m1Customer) { + $email = $m1Customer->email ?? null; + if (!empty($email) && is_string($email)) { + $normalizedEmail = strtolower(trim($email)); + if (!empty($normalizedEmail)) { + $m1EmailsWithCustomers[$normalizedEmail] = $m1Customer; + } + } + } + + Log::info("M1 Customers Not In M2: M1 emails count = " . count($m1EmailsWithCustomers)); + + // Filter M1 customers that don't exist in M2 + $missingCustomers = collect(); + foreach ($m1EmailsWithCustomers as $normalizedEmail => $m1Customer) { + if (!isset($m2EmailsSet[$normalizedEmail])) { + $missingCustomers->push($m1Customer); + } + } + + Log::info("M1 Customers Not In M2: Missing customers count = " . $missingCustomers->count()); + + return $missingCustomers->values(); + } catch (Exception $e) { + Log::error('Error fetching missing customers: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Get Magento 2 customers that don't exist in Magento 1 + */ + public function getM2CustomersNotInM1() + { + try { + $m1Customers = $this->getMagento1Customers(); + $m2Customers = $this->getMagento2Customers(); + + // Get all M1 emails - create a set for faster lookup + $m1EmailsSet = []; + foreach ($m1Customers as $m1Customer) { + $email = $m1Customer->email ?? null; + if (!empty($email) && is_string($email)) { + $normalizedEmail = strtolower(trim($email)); + if (!empty($normalizedEmail)) { + $m1EmailsSet[$normalizedEmail] = true; + } + } + } + + // Filter M2 customers that don't exist in M1 + $missingCustomers = collect(); + foreach ($m2Customers as $m2Customer) { + $email = $m2Customer->email ?? null; + if (!empty($email) && is_string($email)) { + $normalizedEmail = strtolower(trim($email)); + if (!empty($normalizedEmail) && !isset($m1EmailsSet[$normalizedEmail])) { + $missingCustomers->push($m2Customer); + } + } + } + + return $missingCustomers->values(); + } catch (Exception $e) { + Log::error('Error fetching M2 customers not in M1: ' . $e->getMessage()); + Log::error('Stack trace: ' . $e->getTraceAsString()); + return collect([]); + } + } + + /** + * Migrate all customers from Magento 1 to Magento 2 + */ + public function migrateCustomers($dryRun = false) + { + try { + $this->migrationLog = []; + $addedCount = 0; + $updatedCount = 0; + $errorCount = 0; + + // Get M1 and M2 entity type IDs + $m1EntityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'customer') + ->value('entity_type_id'); + + $m2EntityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'customer') + ->value('entity_type_id'); + + if (!$m1EntityTypeId || !$m2EntityTypeId) { + return [ + 'success' => false, + 'message' => 'Entity type not found', + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + + // Get all M1 customers + $m1Customers = $this->getMagento1Customers(); + + // Get all customer attribute IDs from M1 + $m1AttributeIds = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $m1EntityTypeId) + ->pluck('attribute_id', 'attribute_code') + ->toArray(); + + // Get all customer attribute IDs from M2 + $m2AttributeIds = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $m2EntityTypeId) + ->pluck('attribute_id', 'attribute_code') + ->toArray(); + + if (!$dryRun) { + DB::connection($this->magento2Connection)->beginTransaction(); + } + + foreach ($m1Customers as $m1Customer) { + try { + $m1Email = !empty($m1Customer->email) ? strtolower(trim($m1Customer->email)) : null; + + if (empty($m1Email)) { + $this->migrationLog[] = "SKIPPED: Customer ID {$m1Customer->entity_id} - no email address"; + continue; + } + + // Check if customer exists in M2 by email (case-insensitive) + $m2Customer = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'customer_entity') + ->whereRaw('LOWER(TRIM(email)) = ?', [strtolower(trim($m1Email))]) + ->first(); + + $m2CustomerId = null; + $isNew = false; + + if ($m2Customer) { + // Customer exists, update + $m2CustomerId = $m2Customer->entity_id; + if (!$dryRun) { + $this->migrationLog[] = "Updating existing customer: {$m1Email} (ID: {$m2CustomerId})"; + } else { + $this->migrationLog[] = "Would update existing customer: {$m1Email} (ID: {$m2CustomerId})"; + } + $updatedCount++; + } else { + // Customer doesn't exist, create + if (!$dryRun) { + // Insert customer entity + $m2CustomerId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'customer_entity') + ->insertGetId([ + 'email' => $m1Email, + 'website_id' => $m1Customer->website_id ?? 1, + 'group_id' => $m1Customer->group_id ?? 1, + 'created_at' => $m1Customer->created_at ?? now(), + 'updated_at' => $m1Customer->updated_at ?? now(), + ]); + $this->migrationLog[] = "Added new customer: {$m1Email} (ID: {$m2CustomerId})"; + } else { + $this->migrationLog[] = "Would add new customer: {$m1Email}"; + $m2CustomerId = 0; // Placeholder for dry run + } + $addedCount++; + $isNew = true; + } + + if (!$dryRun && $m2CustomerId) { + // Migrate customer attributes + $attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime']; + + foreach ($attributeTables as $tableType) { + $m1Table = $this->magento1Prefix . 'customer_entity_' . $tableType; + $m2Table = $this->magento2Prefix . 'customer_entity_' . $tableType; + + try { + // Get all attributes for this customer from M1 + $m1Attributes = DB::connection($this->magento1Connection) + ->table($m1Table) + ->where('entity_id', $m1Customer->entity_id) + ->get(); + + foreach ($m1Attributes as $m1Attr) { + // Check if attribute exists in M2 + $attributeCode = array_search($m1Attr->attribute_id, $m1AttributeIds); + if ($attributeCode && isset($m2AttributeIds[$attributeCode])) { + $m2AttributeId = $m2AttributeIds[$attributeCode]; + + // Check if attribute value already exists (customer attributes don't have store_id) + $exists = DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2CustomerId) + ->where('attribute_id', $m2AttributeId) + ->exists(); + + if (!$exists) { + // Customer attributes don't have store_id column + $insertData = [ + 'entity_id' => $m2CustomerId, + 'attribute_id' => $m2AttributeId, + 'value' => $m1Attr->value ?? null, + ]; + DB::connection($this->magento2Connection) + ->table($m2Table) + ->insert($insertData); + } else { + // Update existing attribute + DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2CustomerId) + ->where('attribute_id', $m2AttributeId) + ->update(['value' => $m1Attr->value ?? null]); + } + } + } + } catch (Exception $e) { + // Table might not exist, continue + Log::warning("Table {$m1Table} or {$m2Table} might not exist: " . $e->getMessage()); + } + } + } + + } catch (Exception $e) { + $errorCount++; + $m1Email = $m1Customer->email ?? 'N/A'; + $this->migrationLog[] = "ERROR: Failed to migrate customer {$m1Email}: " . $e->getMessage(); + Log::error("Error migrating customer {$m1Email}: " . $e->getMessage()); + } + } + + if (!$dryRun) { + DB::connection($this->magento2Connection)->commit(); + } + + return [ + 'success' => true, + 'message' => $dryRun ? 'Dry run completed' : 'Customer migration completed', + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog + ]; + + } catch (Exception $e) { + if (!$dryRun) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Error migrating customers: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + } + + /** + * Delete a single customer from Magento 2 + */ + public function deleteM2Customer($customerId) + { + try { + DB::connection($this->magento2Connection)->beginTransaction(); + + // Get entity type ID + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'customer') + ->value('entity_type_id'); + + if ($entityTypeId) { + // Delete attribute values from all attribute tables + $attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime']; + foreach ($attributeTables as $tableType) { + $table = $this->magento2Prefix . 'customer_entity_' . $tableType; + try { + DB::connection($this->magento2Connection) + ->table($table) + ->where('entity_id', $customerId) + ->delete(); + } catch (Exception $e) { + Log::warning("Table {$table} might not exist: " . $e->getMessage()); + } + } + } + + // Delete the customer entity + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'customer_entity') + ->where('entity_id', $customerId) + ->delete(); + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => 'Customer deleted successfully' + ]; + + } catch (Exception $e) { + DB::connection($this->magento2Connection)->rollBack(); + Log::error("Error deleting customer {$customerId}: " . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to delete customer: ' . $e->getMessage() + ]; + } + } } diff --git a/resources/js/customers.js b/resources/js/customers.js new file mode 100644 index 0000000..d596c71 --- /dev/null +++ b/resources/js/customers.js @@ -0,0 +1,115 @@ +// Customers page JavaScript + +let routes = {}; +let csrfToken = ''; + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + if (window.customerRoutes) { + routes = window.customerRoutes; + csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; + } +}); + +function startCustomerMigration(dryRun) { + const button = dryRun ? document.getElementById('dryRunCustomerMigrationBtn') : document.getElementById('startCustomerMigrationBtn'); + const otherButton = dryRun ? document.getElementById('startCustomerMigrationBtn') : document.getElementById('dryRunCustomerMigrationBtn'); + const originalText = button.textContent; + button.disabled = true; + otherButton.disabled = true; + button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...'; + button.style.cursor = 'not-allowed'; + + const logContent = document.getElementById('customerMigrationLogContent'); + logContent.innerHTML = '
' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '
'; + + fetch(routes.migrateCustomers, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ dry_run: dryRun }) + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + otherButton.disabled = false; + button.textContent = originalText; + button.style.cursor = 'pointer'; + + if (data.success) { + const successEntry = document.createElement('div'); + successEntry.className = 'log-entry success'; + successEntry.textContent = `✓ ${dryRun ? 'Dry run' : 'Migration'} completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`; + logContent.appendChild(successEntry); + + if (data.log && data.log.length > 0) { + data.log.forEach(log => { + const entry = document.createElement('div'); + entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : 'success'); + entry.textContent = log; + logContent.appendChild(entry); + }); + } + + const logContainer = document.getElementById('customerMigrationLogContainer'); + logContainer.scrollTop = logContainer.scrollHeight; + } else { + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error'); + logContent.appendChild(errorEntry); + } + }) + .catch(error => { + button.disabled = false; + otherButton.disabled = false; + button.textContent = originalText; + button.style.cursor = 'pointer'; + + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ Error: ' + error.message; + logContent.appendChild(errorEntry); + }); +} + +function deleteM2Customer(customerId, email, firstname, lastname) { + const customerName = firstname !== 'N/A' && lastname !== 'N/A' + ? `${firstname} ${lastname}` + : email !== 'N/A' + ? email + : `ID ${customerId}`; + + if (!confirm(`Are you sure you want to delete customer "${customerName}" (Email: ${email})?`)) { + return; + } + + const url = routes.deleteCustomer.replace(':id', customerId); + + fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Customer deleted successfully!'); + location.reload(); + } else { + alert('Error: ' + (data.message || 'Failed to delete customer')); + } + }) + .catch(error => { + alert('Error: ' + error.message); + }); +} + +// Make functions available globally +window.startCustomerMigration = startCustomerMigration; +window.deleteM2Customer = deleteM2Customer; + diff --git a/resources/js/products.js b/resources/js/products.js index ae7f1a3..5097174 100644 --- a/resources/js/products.js +++ b/resources/js/products.js @@ -9,6 +9,12 @@ document.addEventListener('DOMContentLoaded', function() { routes = window.productRoutes; csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; } + + // Attach event listeners to buttons + const fixCategoryProductsBtn = document.getElementById('fixCategoryProductsBtn'); + if (fixCategoryProductsBtn) { + fixCategoryProductsBtn.addEventListener('click', fixCategoryProducts); + } }); function startProductMigration(dryRun) { @@ -190,6 +196,78 @@ function syncProductCategories() { }); } +function fixCategoryProducts() { + if (!confirm('Are you sure you want to fix category products? This will add missing products to the catalog_category_product table based on their category_ids attribute in Magento 2.')) { + return; + } + + const button = document.getElementById('fixCategoryProductsBtn'); + const logContainer = document.getElementById('fixCategoryProductsLogContainer'); + const logContent = document.getElementById('fixCategoryProductsLogContent'); + + if (!button) { + console.error('fixCategoryProductsBtn not found'); + return; + } + + if (!routes.fixCategoryProducts) { + console.error('routes.fixCategoryProducts not found', routes); + alert('Error: Route not configured. Please refresh the page.'); + return; + } + + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Fixing...'; + logContainer.style.display = 'block'; + logContent.innerHTML = '
Starting fix process...
'; + + fetch(routes.fixCategoryProducts, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + button.disabled = false; + button.textContent = originalText; + + if (data.success) { + const successEntry = document.createElement('div'); + successEntry.className = 'log-entry success'; + successEntry.textContent = `✓ Fix completed! Added: ${data.added || 0}, Skipped: ${data.skipped || 0}, Errors: ${data.errors || 0}`; + logContent.appendChild(successEntry); + + if (data.log && data.log.length > 0) { + data.log.forEach(log => { + const entry = document.createElement('div'); + entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : (log.includes('ADDED') ? 'success' : 'info')); + entry.textContent = log; + logContent.appendChild(entry); + }); + } + + logContent.scrollTop = logContent.scrollHeight; + } else { + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ Fix failed: ' + (data.message || 'Unknown error'); + logContent.appendChild(errorEntry); + } + }) + .catch(error => { + button.disabled = false; + button.textContent = originalText; + + const errorEntry = document.createElement('div'); + errorEntry.className = 'log-entry error'; + errorEntry.textContent = '✗ Error: ' + error.message; + logContent.appendChild(errorEntry); + }); +} + function loadM1CategoryTreeWithProducts() { const container = document.getElementById('m1-category-products-tree-container'); if (!container) return; @@ -353,6 +431,7 @@ window.startProductMigration = startProductMigration; window.deleteM2Product = deleteM2Product; window.deleteProductsAboveM1Max = deleteProductsAboveM1Max; window.syncProductCategories = syncProductCategories; +window.fixCategoryProducts = fixCategoryProducts; window.loadM1CategoryTreeWithProducts = loadM1CategoryTreeWithProducts; window.loadM2CategoryTreeWithProducts = loadM2CategoryTreeWithProducts; diff --git a/resources/views/customers/index.blade.php b/resources/views/customers/index.blade.php new file mode 100644 index 0000000..f9a9a6a --- /dev/null +++ b/resources/views/customers/index.blade.php @@ -0,0 +1,161 @@ +@extends('layouts.app') + +@section('content') + +
+

👥 Customer Migration

+
+

What happens when you click "Start Customer Migration"?

+

The customer migration process will:

+
    +
  • Create new customers: If a customer with the same email doesn't exist in Magento 2, it will be created with all its attributes
  • +
  • Update existing customers: If a customer with the same email already exists in Magento 2, it will be updated with the latest data from Magento 1
  • +
  • Migrate customer attributes: All customer attributes including email, firstname, lastname, and other custom attributes will be migrated
  • +
  • Preserve customer data: Website ID, group ID, and timestamps will be preserved
  • +
  • Generate logs: Detailed migration logs showing which customers were added, updated, or encountered errors
  • +
+

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

+
+ +
+ + +
+
+ + +
+

📋 Customer Migration Logs

+

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

+
+
+
+ No customer migration logs yet. Click "Run Dry Run" or "Start Customer Migration" to begin. +
+
+
+
+ + +
+

📊 Customer Statistics

+
+
+
{{ $m1Customers->count() }}
+
Magento 1 Customers
+
+
+
{{ $m2Customers->count() }}
+
Magento 2 Customers
+
+
+
{{ $m1CustomersNotInM2->count() }}
+
M1 Customers Not in M2
+
+
+
{{ $m2CustomersNotInM1->count() }}
+
M2 Customers Not in M1
+
+
+
+ + + @if($m1CustomersNotInM2->count() > 0) +
+

⚠️ Magento 1 Customers Not in Magento 2

+

These customers exist in Magento 1 but are missing in Magento 2:

+
+ + + + + + + + + + + + + @foreach($m1CustomersNotInM2->take(50) as $customer) + + + + + + + + + @endforeach + +
IDEmailFirst NameLast NameWebsite IDGroup ID
{{ $customer->entity_id }}{{ $customer->email ?? 'N/A' }}{{ $customer->firstname ?? 'N/A' }}{{ $customer->lastname ?? 'N/A' }}{{ $customer->website_id ?? 'N/A' }}{{ $customer->group_id ?? 'N/A' }}
+ @if($m1CustomersNotInM2->count() > 50) +

Showing first 50 of {{ $m1CustomersNotInM2->count() }} customers.

+ @endif +
+
+ @endif + + + @if($m2CustomersNotInM1->count() > 0) +
+

⚠️ Magento 2 Customers Not in Magento 1

+

These customers exist in Magento 2 but are missing in Magento 1:

+
+ + + + + + + + + + + + + + @foreach($m2CustomersNotInM1->take(50) as $customer) + + + + + + + + + + @endforeach + +
IDEmailFirst NameLast NameWebsite IDGroup IDActions
{{ $customer->entity_id }}{{ $customer->email ?? 'N/A' }}{{ $customer->firstname ?? 'N/A' }}{{ $customer->lastname ?? 'N/A' }}{{ $customer->website_id ?? 'N/A' }}{{ $customer->group_id ?? 'N/A' }} + +
+ @if($m2CustomersNotInM1->count() > 50) +

Showing first 50 of {{ $m2CustomersNotInM1->count() }} customers.

+ @endif +
+
+ @endif +@endsection + +{{-- CSS is loaded globally via app.css --}} + +@push('scripts') + @vite(['resources/js/customers.js']) + +@endpush + diff --git a/resources/views/partials/navigation.blade.php b/resources/views/partials/navigation.blade.php index 298ee21..b0a16ed 100644 --- a/resources/views/partials/navigation.blade.php +++ b/resources/views/partials/navigation.blade.php @@ -14,5 +14,8 @@ Products + + Customers + diff --git a/resources/views/products/index.blade.php b/resources/views/products/index.blade.php index 02ef86c..57d2978 100644 --- a/resources/views/products/index.blade.php +++ b/resources/views/products/index.blade.php @@ -185,6 +185,30 @@ class="btn btn-danger" + +
+

🔧 Fix Category Products

+
+

What does this do?

+

This tool checks the Magento 2 catalog_category_product table and ensures products are added to categories if they are missing:

+
    +
  • Finds missing products: Identifies products in Magento 2 that are not in the catalog_category_product table
  • +
  • Checks category_ids attribute: For each missing product, reads the category_ids attribute value
  • +
  • Adds to categories: Adds the product to the categories specified in its category_ids attribute
  • +
  • Validates categories: Only adds products to categories that exist in Magento 2
  • +
+

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

+
+ + +
+

🌳 Category Tree with Products

@@ -226,6 +250,7 @@ class="btn btn-danger" deleteProduct: '{{ route("products.delete-product", ["productId" => ":id"]) }}', deleteProductsAboveM1Max: '{{ route("products.delete-products-above-m1-max") }}', syncProductCategories: '{{ route("products.sync-product-categories") }}', + fixCategoryProducts: '{{ route("products.fix-category-products") }}', magento1CategoryTreeWithProducts: '{{ route("categories.magento1-category-tree-with-products") }}', magento2CategoryTreeWithProducts: '{{ route("categories.magento2-category-tree-with-products") }}' }; diff --git a/routes/web.php b/routes/web.php index 470755e..688f7c4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,6 +6,7 @@ use App\Http\Controllers\MigrationController; use App\Http\Controllers\AttributesController; use App\Http\Controllers\ProductsController; +use App\Http\Controllers\CustomersController; Route::get('/', function () { return redirect('/connections'); @@ -50,6 +51,14 @@ Route::get('/', [ProductsController::class, 'index'])->name('index'); Route::post('/migrate', [ProductsController::class, 'migrateProducts'])->name('migrate-products'); Route::post('/sync-categories', [ProductsController::class, 'syncProductCategories'])->name('sync-product-categories'); + Route::post('/fix-category-products', [ProductsController::class, 'fixCategoryProducts'])->name('fix-category-products'); Route::delete('/{productId}', [ProductsController::class, 'deleteM2Product'])->name('delete-product'); Route::delete('/above-m1-max', [ProductsController::class, 'deleteM2ProductsAboveM1Max'])->name('delete-products-above-m1-max'); }); + +// Customers routes +Route::prefix('customers')->name('customers.')->group(function () { + Route::get('/', [CustomersController::class, 'index'])->name('index'); + Route::post('/migrate', [CustomersController::class, 'migrateCustomers'])->name('migrate-customers'); + Route::delete('/{customerId}', [CustomersController::class, 'deleteM2Customer'])->name('delete-customer'); +}); diff --git a/vite.config.js b/vite.config.js index c41afea..22e06cf 100644 --- a/vite.config.js +++ b/vite.config.js @@ -13,6 +13,7 @@ export default defineConfig({ 'resources/js/migration.js', 'resources/js/attributes.js', 'resources/js/products.js', + 'resources/js/customers.js', ], refresh: true, }),