magento1Prefix = config('database.connections.magento1.prefix', ''); $this->magento2Prefix = config('database.connections.magento2.prefix', ''); } /** * Compare M1 text attributes (potential tab content) against M2 mgs_protabs config. * Returns per-scope breakdown of what is configured and what is missing. */ public function compareProtabs(Request $request) { try { // --- M2: gather all Protabs entries and website/store names --- $m2Tabs = DB::connection($this->magento2Connection) ->table('mgs_protabs') ->orderBy('scope') ->orderBy('scope_id') ->orderBy('position') ->get(); $m2Websites = DB::connection($this->magento2Connection) ->table('store_website') ->whereNotIn('website_id', [0]) ->whereRaw("name NOT LIKE '%Admin%'") ->pluck('name', 'website_id'); $m2Stores = DB::connection($this->magento2Connection) ->table('store') ->where('store_id', '>', 0) ->pluck('name', 'store_id'); // Tabs keyed by "scope:scope_id" for easy lookup $tabsByScope = []; foreach ($m2Tabs as $tab) { $key = $tab->scope . ':' . $tab->scope_id; $tabsByScope[$key][] = (array) $tab; } // --- M1: find all catalog_product text attributes with values --- $entityTypeIdM1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_entity_type') ->where('entity_type_code', 'catalog_product') ->value('entity_type_id'); // Get all text-backend attributes that actually have product values $m1TextAttrs = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_attribute as a') ->join($this->magento1Prefix . 'catalog_product_entity_text as v', 'a.attribute_id', '=', 'v.attribute_id') ->where('a.entity_type_id', $entityTypeIdM1) ->select('a.attribute_code', 'a.frontend_label', DB::raw('COUNT(v.value_id) as value_count')) ->whereNotNull('v.value') ->where('v.value', '!=', '') ->groupBy('a.attribute_id', 'a.attribute_code', 'a.frontend_label') ->having('value_count', '>', 0) ->orderBy('a.attribute_code') ->get() ->keyBy('attribute_code'); // --- Build per-scope comparison --- // Collect all unique scopes from existing tabs $scopes = $m2Tabs->map(fn($t) => ['scope' => $t->scope, 'scope_id' => $t->scope_id]) ->unique(fn($s) => $s['scope'] . ':' . $s['scope_id']) ->values(); $comparison = []; foreach ($scopes as $scope) { $key = $scope['scope'] . ':' . $scope['scope_id']; $existing = $tabsByScope[$key] ?? []; $existingAttrCodes = collect($existing) ->where('tab_type', 'attribute') ->pluck('value') ->filter() ->flip(); // use as set // Find M1 text attrs with values not in this scope's protabs $missing = []; foreach ($m1TextAttrs as $attrCode => $attr) { if (!isset($existingAttrCodes[$attrCode])) { $missing[] = [ 'attribute_code' => $attrCode, 'frontend_label' => $attr->frontend_label ?? $attrCode, 'value_count' => $attr->value_count, 'suggested_title' => ucwords(str_replace('_', ' ', $attrCode)), 'suggested_pos' => count($existing) + count($missing) + 1, ]; } } $scopeLabel = match ($scope['scope']) { 'default' => 'Default (all stores)', 'websites' => 'Website: ' . ($m2Websites[$scope['scope_id']] ?? 'ID ' . $scope['scope_id']), 'stores' => 'Store: ' . ($m2Stores[$scope['scope_id']] ?? 'ID ' . $scope['scope_id']), default => $scope['scope'] . ' ' . $scope['scope_id'], }; $comparison[] = [ 'scope' => $scope['scope'], 'scope_id' => $scope['scope_id'], 'scope_label' => $scopeLabel, 'existing' => $existing, 'missing' => $missing, ]; } return response()->json([ 'success' => true, 'comparison' => $comparison, 'm1_text_attr_count' => $m1TextAttrs->count(), ]); } catch (\Throwable $e) { Log::error('Protabs compare error: ' . $e->getMessage()); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } /** * Insert one or more missing tabs into M2 mgs_protabs. * Accepts an array of tab objects: [{title, tab_type, value, position, scope, scope_id}, ...] */ public function syncProtabs(Request $request) { $tabs = $request->input('tabs', []); if (empty($tabs)) { return response()->json(['success' => false, 'message' => 'No tabs provided.'], 400); } $inserted = 0; $skipped = 0; $errors = 0; $log = []; foreach ($tabs as $tab) { $scope = $tab['scope'] ?? 'default'; $scopeId = (int)($tab['scope_id'] ?? 0); $value = trim($tab['value'] ?? ''); $title = trim($tab['title'] ?? ''); $tabType = $tab['tab_type'] ?? 'attribute'; $pos = (int)($tab['position'] ?? 99); if ($value === '' || $title === '') { $log[] = "Skipped entry with empty value or title."; $skipped++; continue; } // Check if already exists for this scope $exists = DB::connection($this->magento2Connection) ->table('mgs_protabs') ->where('scope', $scope) ->where('scope_id', $scopeId) ->where('tab_type', $tabType) ->where('value', $value) ->exists(); if ($exists) { $log[] = "Tab '{$title}' ({$value}) already exists for {$scope}:{$scopeId}, skipped."; $skipped++; continue; } try { DB::connection($this->magento2Connection) ->table('mgs_protabs') ->insert([ 'title' => $title, 'tab_type' => $tabType, 'value' => $value, 'position' => $pos, 'scope' => $scope, 'scope_id' => $scopeId, ]); $inserted++; $log[] = "Added tab '{$title}' ({$value}) to {$scope}:{$scopeId} at position {$pos}."; } catch (\Throwable $e) { $errors++; $log[] = "Error adding '{$value}': " . $e->getMessage(); Log::error('Protabs sync error: ' . $e->getMessage()); } } return response()->json([ 'success' => $errors === 0, 'message' => "Protabs sync: {$inserted} added, {$skipped} skipped, {$errors} errors.", 'inserted' => $inserted, 'skipped' => $skipped, 'errors' => $errors, 'log' => $log, ]); } /** * Show the Additional tools page */ public function index() { $frontpageTabsInfo = $this->getFrontpageTabsAttributeInfo(); return view('additional.index', [ 'frontpageTabsInfo' => $frontpageTabsInfo, ]); } /** * Get info about the frontpage tabs attribute in M1 and M2 (for display / discovery) */ protected function getFrontpageTabsAttributeInfo() { $info = ['m1' => null, 'm2' => null, 'suggested_codes' => []]; try { $entityTypeIdM1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_entity_type') ->where('entity_type_code', 'catalog_product') ->value('entity_type_id'); $entityTypeIdM2 = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_entity_type') ->where('entity_type_code', 'catalog_product') ->value('entity_type_id'); if (!$entityTypeIdM1 || !$entityTypeIdM2) { return $info; } foreach (['frontpage_tabs', 'front_tabs', 'product_tabs'] as $code) { $m1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM1) ->where('attribute_code', $code) ->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_label') ->first(); $m2 = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM2) ->where('attribute_code', $code) ->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_label') ->first(); if ($m1) { $info['m1'] = $m1; $info['m2'] = $m2; break; } if ($m1 || $m2) { $info['suggested_codes'][] = $code; } } if (!$info['m1']) { $suggested = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM1) ->where(function ($q) { $q->where('attribute_code', 'like', '%frontpage%') ->orWhere('attribute_code', 'like', '%front%tab%') ->orWhere('attribute_code', 'like', '%tab%'); }) ->pluck('attribute_code') ->toArray(); $info['suggested_codes'] = array_unique(array_merge($info['suggested_codes'], $suggested)); } } catch (\Throwable $e) { Log::warning('Frontpage tabs attribute info: ' . $e->getMessage()); } return $info; } /** * Return diagnostic info for an attribute code (M1/M2 existence, backend_type, value row counts) */ public function attributeDiagnostic(Request $request) { $attributeCode = trim((string) $request->input('attribute_code', '')) ?: 'frontpage_tabs'; try { $entityTypeIdM1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_entity_type') ->where('entity_type_code', 'catalog_product') ->value('entity_type_id'); $entityTypeIdM2 = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_entity_type') ->where('entity_type_code', 'catalog_product') ->value('entity_type_id'); if (!$entityTypeIdM1 || !$entityTypeIdM2) { return response()->json(['success' => false, 'message' => 'Could not resolve entity type.', 'm1' => null, 'm2' => null]); } $attrM1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM1) ->where('attribute_code', $attributeCode) ->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_input', 'frontend_label') ->first(); $attrM2 = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM2) ->where('attribute_code', $attributeCode) ->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_input', 'frontend_label') ->first(); $diagnostic = ['attribute_code' => $attributeCode, 'm1' => null, 'm2' => null]; if ($attrM1) { $diagnostic['m1'] = (array) $attrM1; $bt = $attrM1->backend_type ?? 'varchar'; $tables = ['varchar', 'text', 'int', 'decimal', 'datetime']; $diagnostic['m1']['value_counts_by_table'] = []; foreach ($tables as $t) { try { $c = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'catalog_product_entity_' . $t) ->where('attribute_id', $attrM1->attribute_id) ->count(); $diagnostic['m1']['value_counts_by_table'][$t] = $c; } catch (\Throwable $e) { $diagnostic['m1']['value_counts_by_table'][$t] = 'error'; } } } if ($attrM2) { $diagnostic['m2'] = (array) $attrM2; } return response()->json(['success' => true, 'diagnostic' => $diagnostic]); } catch (\Throwable $e) { Log::warning('Attribute diagnostic: ' . $e->getMessage()); return response()->json(['success' => false, 'message' => $e->getMessage(), 'diagnostic' => null], 500); } } /** * Sync frontpage tabs from Magento 1 to Magento 2 by SKU * Scans all M1 products by SKU and ensures M2 products have the same frontpage tabs value(s) */ public function syncFrontpageTabs(Request $request) { $attributeCode = $request->input('attribute_code', $this->frontpageTabsAttributeCode); $dryRun = $request->boolean('dry_run', false); try { $entityTypeIdM1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_entity_type') ->where('entity_type_code', 'catalog_product') ->value('entity_type_id'); $entityTypeIdM2 = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_entity_type') ->where('entity_type_code', 'catalog_product') ->value('entity_type_id'); if (!$entityTypeIdM1 || !$entityTypeIdM2) { return response()->json([ 'success' => false, 'message' => 'Could not resolve catalog_product entity type in M1 or M2.', 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], ], 400); } $attrM1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM1) ->where('attribute_code', $attributeCode) ->first(); if (!$attrM1) { return response()->json([ 'success' => false, 'message' => "Attribute '{$attributeCode}' not found in Magento 1.", 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], ], 400); } $attrM2 = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM2) ->where('attribute_code', $attributeCode) ->first(); if (!$attrM2) { return response()->json([ 'success' => false, 'message' => "Attribute '{$attributeCode}' not found in Magento 2. Create the attribute in M2 first.", 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], ], 400); } $backendType = $attrM1->backend_type ?? 'varchar'; if (!in_array($backendType, ['varchar', 'text', 'int', 'decimal', 'datetime'], true)) { $backendType = 'varchar'; } $backendTypesToTry = ['varchar', 'text', 'int', 'decimal', 'datetime']; $m1ValueTable = null; $m2ValueTable = null; $resolvedBackendType = null; foreach ($backendTypesToTry as $bt) { $t1 = $this->magento1Prefix . 'catalog_product_entity_' . $bt; $count = 0; try { $count = DB::connection($this->magento1Connection) ->table($t1) ->where('attribute_id', $attrM1->attribute_id) ->count(); } catch (\Throwable $e) { continue; } if ($count > 0) { $resolvedBackendType = $bt; $m1ValueTable = $t1; $m2ValueTable = $this->magento2Prefix . 'catalog_product_entity_' . $bt; break; } } if ($m1ValueTable === null) { $m1ValueTable = $this->magento1Prefix . 'catalog_product_entity_' . $backendType; $m2ValueTable = $this->magento2Prefix . 'catalog_product_entity_' . $backendType; $resolvedBackendType = $backendType; } $m1ValueRowCount = 0; try { $m1ValueRowCount = DB::connection($this->magento1Connection) ->table($m1ValueTable) ->where('attribute_id', $attrM1->attribute_id) ->count(); } catch (\Throwable $e) { return response()->json([ 'success' => false, 'message' => "Magento 1 value table for attribute '{$attributeCode}' (backend_type {$resolvedBackendType}) not found or error: " . $e->getMessage(), 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], 'diagnostic' => ['backend_type' => $resolvedBackendType, 'm1_value_table' => $m1ValueTable], ], 400); } if ($m1ValueRowCount === 0) { return response()->json([ 'success' => false, 'message' => "No values found in Magento 1 for attribute '{$attributeCode}'. No products have this attribute set in M1. Check that the attribute is assigned to product attribute sets and that products have values saved.", 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], 'diagnostic' => [ 'backend_type' => $resolvedBackendType, 'm1_value_table' => $m1ValueTable, 'm1_value_row_count' => 0, ], ], 400); } $skuAttrIdM1 = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM1) ->where('attribute_code', 'sku') ->value('attribute_id'); $skuAttrIdM2 = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_attribute') ->where('entity_type_id', $entityTypeIdM2) ->where('attribute_code', 'sku') ->value('attribute_id'); if (!$skuAttrIdM1 || !$skuAttrIdM2) { return response()->json([ 'success' => false, 'message' => 'Could not resolve SKU attribute in M1 or M2.', 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], ], 400); } $m1ProductsWithTabs = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'catalog_product_entity as e') ->leftJoin($this->magento1Prefix . 'catalog_product_entity_varchar as sku_v', function ($join) use ($skuAttrIdM1) { $join->on('e.entity_id', '=', 'sku_v.entity_id') ->where('sku_v.attribute_id', '=', $skuAttrIdM1); }) ->join($m1ValueTable . ' as vt', function ($join) use ($attrM1) { $join->on('e.entity_id', '=', 'vt.entity_id') ->where('vt.attribute_id', '=', $attrM1->attribute_id); }) ->where(function ($q) { $q->whereNotNull('sku_v.value') ->orWhereNotNull('e.sku'); }) ->selectRaw('e.entity_id as m1_entity_id, TRIM(COALESCE(sku_v.value, e.sku)) as sku, vt.store_id, vt.value') ->get(); if ($m1ProductsWithTabs->isEmpty()) { return response()->json([ 'success' => false, 'message' => "Attribute '{$attributeCode}' has {$m1ValueRowCount} value row(s) in M1, but no rows matched when joining with products (by SKU). Possible causes: SKU is stored in a different store_id in M1, or the value table does not match the attribute's backend_type.", 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], 'diagnostic' => [ 'backend_type' => $resolvedBackendType, 'm1_value_table' => $m1ValueTable, 'm1_value_row_count' => $m1ValueRowCount, ], ], 400); } $m2SkuToEntityId = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'catalog_product_entity as e') ->leftJoin($this->magento2Prefix . 'catalog_product_entity_varchar as sku_v', function ($join) use ($skuAttrIdM2) { $join->on('e.entity_id', '=', 'sku_v.entity_id') ->where('sku_v.attribute_id', '=', $skuAttrIdM2); }) ->where(function ($q) { $q->whereNotNull('sku_v.value') ->orWhereNotNull('e.sku'); }) ->selectRaw('e.entity_id, TRIM(COALESCE(sku_v.value, e.sku)) as sku') ->pluck('e.entity_id', 'sku'); $log = []; $updated = 0; $skipped = 0; $skippedNoM2 = 0; $skippedAlreadyMatch = 0; $errors = 0; foreach ($m1ProductsWithTabs as $row) { $sku = trim((string) $row->sku); $m2EntityId = $m2SkuToEntityId->get($sku); if ($m2EntityId === null) { $skipped++; $skippedNoM2++; $log[] = "SKU {$sku}: no M2 product, skipped."; continue; } $existing = DB::connection($this->magento2Connection) ->table($m2ValueTable) ->where('entity_id', $m2EntityId) ->where('attribute_id', $attrM2->attribute_id) ->where('store_id', $row->store_id) ->first(); if ($existing && (string) $existing->value === (string) $row->value) { $skipped++; $skippedAlreadyMatch++; $log[] = "SKU {$sku} (store {$row->store_id}): M2 value already matches M1, skipped."; continue; } if ($dryRun) { $updated++; $log[] = "SKU {$sku} (store {$row->store_id}): would set value."; continue; } try { if ($existing) { DB::connection($this->magento2Connection) ->table($m2ValueTable) ->where('entity_id', $m2EntityId) ->where('attribute_id', $attrM2->attribute_id) ->where('store_id', $row->store_id) ->update(['value' => $row->value]); } else { DB::connection($this->magento2Connection) ->table($m2ValueTable) ->insert([ 'attribute_id' => $attrM2->attribute_id, 'store_id' => $row->store_id, 'entity_id' => $m2EntityId, 'value' => $row->value, ]); } $updated++; $log[] = "SKU {$sku} (store {$row->store_id}): synced."; } catch (\Throwable $e) { $errors++; $log[] = "SKU {$sku}: error - " . $e->getMessage(); Log::error("Sync frontpage tabs SKU {$sku}: " . $e->getMessage()); } } $m1SampleSkus = $m1ProductsWithTabs->take(5)->pluck('sku')->unique()->values()->toArray(); $diagnostic = [ 'backend_type' => $resolvedBackendType, 'm1_value_row_count' => $m1ValueRowCount, 'm1_products_with_value' => $m1ProductsWithTabs->count(), 'skipped_no_m2_product' => $skippedNoM2, 'skipped_already_match' => $skippedAlreadyMatch, 'm1_sample_skus' => $m1SampleSkus, ]; return response()->json([ 'success' => true, 'message' => $dryRun ? "Dry run: {$updated} product/store rows would be updated, {$skipped} skipped." : "Synced frontpage tabs: {$updated} updated, {$skipped} skipped, {$errors} errors.", 'updated' => $updated, 'skipped' => $skipped, 'errors' => $errors, 'log' => array_slice($log, -100), 'diagnostic' => $diagnostic, ]); } catch (\Throwable $e) { Log::error('Sync frontpage tabs error: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Sync failed: ' . $e->getMessage(), 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'log' => [], ], 500); } } }