Implement Google Price Insights with update functionality

Major rewrite using correct Google Merchant API:
- Use price_insights_product_view table (correct API endpoint)
- Fetch suggested_price and predicted performance changes
- Show predicted impact on impressions, clicks, conversions

New features:
- Individual "Update" button per product
- Bulk update with checkbox selection
- Pagination (50 products per page)
- Sort by potential gain (highest first)

Price handling:
- Always use tax-inclusive prices for comparison with Google
- Convert back to store format when saving (handles tax-exclusive stores)
- Set as sale price when updating

UI improvements:
- Color-coded gain/loss values
- Color-coded predicted changes
- Summary stats showing products that can increase/decrease
- Total potential gain calculation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 18:22:17 +01:00
parent 1b12eb53d0
commit 53185fd49e
3 changed files with 546 additions and 322 deletions

View File

@@ -2,20 +2,14 @@
* Admin JavaScript for Informatiq Smart Pricing
*/
console.log('Informatiq Smart Pricing: Script loaded');
(function($) {
'use strict';
console.log('Informatiq Smart Pricing: IIFE executed, jQuery available:', typeof $ !== 'undefined');
var InformatiqSP = {
/**
* Initialize
*/
init: function() {
console.log('Informatiq Smart Pricing: init() called');
console.log('informatiqSP object:', typeof informatiqSP !== 'undefined' ? informatiqSP : 'NOT DEFINED');
this.bindEvents();
},
@@ -23,17 +17,10 @@ console.log('Informatiq Smart Pricing: Script loaded');
* Bind event handlers
*/
bindEvents: function() {
console.log('Informatiq Smart Pricing: bindEvents() called');
var $compareBtn = $('#informatiq-sp-compare-products');
console.log('Compare products button found:', $compareBtn.length > 0);
$('#informatiq-sp-manual-sync').on('click', this.handleManualSync);
$('#informatiq-sp-test-connection').on('click', this.handleTestConnection);
$('#informatiq-sp-revoke-auth').on('click', this.handleRevokeAuth);
$compareBtn.on('click', this.handleCompareProducts);
console.log('Informatiq Smart Pricing: All event handlers bound');
$('#informatiq-sp-compare-products').on('click', this.handleCompareProducts);
},
/**
@@ -195,167 +182,50 @@ console.log('Informatiq Smart Pricing: Script loaded');
});
},
// Store comparison data for pagination and bulk updates.
comparisonData: null,
currentPage: 1,
perPage: 50,
/**
* Handle compare products button click
*/
handleCompareProducts: function(e) {
e.preventDefault();
console.log('=== COMPARE PRODUCTS CLICKED ===');
var self = InformatiqSP;
var $button = $(this);
var $spinner = $button.next('.spinner');
var $results = $('#informatiq-sp-comparison-results');
var $tbody = $('#informatiq-sp-comparison-tbody');
var $status = $('#informatiq-sp-sync-status');
console.log('Button:', $button.length);
console.log('Spinner:', $spinner.length);
console.log('Results container:', $results.length);
console.log('informatiqSP:', informatiqSP);
// Show loading status
$status
.removeClass('notice-success notice-error')
.addClass('notice-info')
.html('<p><strong>Loading product comparison...</strong> This may take a moment.</p>')
.html('<p><strong>Loading price insights from Google...</strong> This may take a moment.</p>')
.show();
// Disable button and show spinner
$button.prop('disabled', true);
$spinner.addClass('is-active');
console.log('Making AJAX request to:', informatiqSP.ajaxUrl);
console.log('With nonce:', informatiqSP.nonce);
// Make AJAX request
$.ajax({
url: informatiqSP.ajaxUrl,
type: 'POST',
timeout: 120000, // 2 minute timeout
timeout: 180000, // 3 minute timeout
data: {
action: 'informatiq_sp_compare_products',
nonce: informatiqSP.nonce
},
beforeSend: function(xhr) {
console.log('AJAX beforeSend - request starting');
},
success: function(response) {
console.log('AJAX success callback');
console.log('AJAX response:', response);
// Log debug info if available.
if (response.data && response.data.debug) {
console.log('=== DEBUG INFO ===');
console.log('Sample Google product:', response.data.debug.sample_google_product);
console.log('Sample indexed keys:', response.data.debug.sample_google_keys);
console.log('Total indexed entries:', response.data.debug.index_count);
console.log('==================');
}
console.log('Price insights response:', response);
$status.hide();
if (response.success) {
var data = response.data;
var html = '';
// Stats counters.
var stats = {
total: 0,
withBenchmark: 0,
cheaper: 0,
competitive: 0,
expensive: 0,
totalPotentialGain: 0
};
if (data.products.length === 0) {
html = '<tr><td colspan="6">No in-stock products with SKU found.</td></tr>';
} else {
$.each(data.products, function(i, product) {
stats.total++;
var localPrice = product.local_price ? data.currency + parseFloat(product.local_price).toFixed(2) : '-';
var benchmarkPrice = product.benchmark_price ? data.currency + parseFloat(product.benchmark_price).toFixed(2) : '-';
var recommendedPrice = product.recommended_price ? data.currency + parseFloat(product.recommended_price).toFixed(2) : '-';
var potentialGain = '-';
var priceLabel = product.price_type === 'sale' ? ' <small>(sale)</small>' : '';
// Status and colors.
var statusText = '-';
var statusStyle = '';
var localPriceStyle = '';
var gainStyle = '';
if (product.has_benchmark) {
stats.withBenchmark++;
if (product.price_status === 'cheaper') {
stats.cheaper++;
statusText = 'Cheaper';
statusStyle = 'color: #00a32a; font-weight: bold;';
localPriceStyle = 'color: #00a32a;';
} else if (product.price_status === 'competitive') {
stats.competitive++;
statusText = 'Competitive';
statusStyle = 'color: #2271b1; font-weight: bold;';
localPriceStyle = 'color: #2271b1;';
} else if (product.price_status === 'expensive') {
stats.expensive++;
statusText = 'Expensive';
statusStyle = 'color: #d63638; font-weight: bold;';
localPriceStyle = 'color: #d63638;';
}
if (product.potential_gain !== null) {
var gain = parseFloat(product.potential_gain);
stats.totalPotentialGain += gain;
if (gain > 0) {
potentialGain = '+' + data.currency + gain.toFixed(2);
gainStyle = 'color: #00a32a; font-weight: bold;';
} else if (gain < 0) {
potentialGain = '-' + data.currency + Math.abs(gain).toFixed(2);
gainStyle = 'color: #d63638;';
} else {
potentialGain = data.currency + '0.00';
}
}
} else if (product.found) {
statusText = 'No benchmark';
statusStyle = 'color: #dba617;';
} else {
statusText = 'Not in Google';
statusStyle = 'color: #888;';
}
html += '<tr>';
html += '<td><a href="post.php?post=' + product.id + '&action=edit" title="SKU: ' + product.sku + '">' + product.name + '</a></td>';
html += '<td style="' + localPriceStyle + '">' + localPrice + priceLabel + '</td>';
html += '<td>' + benchmarkPrice + '</td>';
html += '<td>' + recommendedPrice + '</td>';
html += '<td style="' + gainStyle + '">' + potentialGain + '</td>';
html += '<td style="' + statusStyle + '">' + statusText + '</td>';
html += '</tr>';
});
}
$tbody.html(html);
self.comparisonData = response.data;
self.currentPage = 1;
self.renderComparison();
$results.show();
// Show summary with statistics.
var $summary = $('#informatiq-sp-comparison-summary');
var summaryHtml = '<strong>Summary:</strong> ';
summaryHtml += stats.withBenchmark + ' of ' + stats.total + ' products have competitor data. ';
summaryHtml += '<span style="color:#00a32a;">' + stats.cheaper + ' cheaper</span>, ';
summaryHtml += '<span style="color:#2271b1;">' + stats.competitive + ' competitive</span>, ';
summaryHtml += '<span style="color:#d63638;">' + stats.expensive + ' expensive</span>. ';
if (stats.totalPotentialGain > 0) {
summaryHtml += '<br><strong style="color:#00a32a;">Total potential gain if optimized: +' + data.currency + stats.totalPotentialGain.toFixed(2) + ' per sale</strong>';
} else if (stats.totalPotentialGain < 0) {
summaryHtml += '<br><strong>Current margin vs recommended: ' + data.currency + stats.totalPotentialGain.toFixed(2) + '</strong>';
}
$summary.html(summaryHtml).show();
} else {
$status
.removeClass('notice-info notice-success')
@@ -365,20 +235,275 @@ console.log('Informatiq Smart Pricing: Script loaded');
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('AJAX error:', textStatus, errorThrown, jqXHR.responseText);
console.error('AJAX error:', textStatus, errorThrown);
$status
.removeClass('notice-info notice-success')
.addClass('notice-error')
.html('<p>Error: ' + errorThrown + ' - ' + textStatus + '</p>')
.html('<p>Error: ' + errorThrown + '</p>')
.show();
},
complete: function() {
console.log('AJAX request complete');
$button.prop('disabled', false);
$spinner.removeClass('is-active');
}
});
}
},
/**
* Render comparison table with pagination
*/
renderComparison: function() {
var self = this;
var data = this.comparisonData;
var $tbody = $('#informatiq-sp-comparison-tbody');
var $summary = $('#informatiq-sp-comparison-summary');
var $pagination = $('#informatiq-sp-pagination');
if (!data || !data.products) return;
var products = data.products;
var totalPages = Math.ceil(products.length / this.perPage);
var start = (this.currentPage - 1) * this.perPage;
var end = start + this.perPage;
var pageProducts = products.slice(start, end);
// Stats
var stats = {
total: products.length,
withInsight: 0,
canIncrease: 0,
shouldDecrease: 0,
totalPotentialGain: 0
};
products.forEach(function(p) {
if (p.has_insight) {
stats.withInsight++;
if (p.potential_gain > 0) {
stats.canIncrease++;
stats.totalPotentialGain += p.potential_gain;
} else if (p.potential_gain < 0) {
stats.shouldDecrease++;
}
}
});
// Render summary
var summaryHtml = '<strong>Summary:</strong> ' + stats.withInsight + ' of ' + stats.total + ' products have Google price insights. ';
summaryHtml += '<span style="color:#00a32a;">' + stats.canIncrease + ' can increase price</span>, ';
summaryHtml += '<span style="color:#d63638;">' + stats.shouldDecrease + ' should decrease price</span>. ';
if (stats.totalPotentialGain > 0) {
summaryHtml += '<br><strong style="color:#00a32a;">Total potential gain: +' + data.currency + stats.totalPotentialGain.toFixed(2) + ' per sale cycle</strong>';
}
$summary.html(summaryHtml);
// Render table rows
var html = '';
if (pageProducts.length === 0) {
html = '<tr><td colspan="9">No products found.</td></tr>';
} else {
pageProducts.forEach(function(product) {
var localPrice = product.local_price ? data.currency + parseFloat(product.local_price).toFixed(2) : '-';
var suggestedPrice = product.suggested_price ? data.currency + parseFloat(product.suggested_price).toFixed(2) : '-';
var priceLabel = product.price_type === 'sale' ? ' <small>(sale)</small>' : '';
// Gain/loss styling
var gainHtml = '-';
var gainStyle = '';
if (product.potential_gain !== null) {
var gain = parseFloat(product.potential_gain);
if (gain > 0) {
gainHtml = '+' + data.currency + gain.toFixed(2);
gainStyle = 'color: #00a32a; font-weight: bold;';
} else if (gain < 0) {
gainHtml = '-' + data.currency + Math.abs(gain).toFixed(2);
gainStyle = 'color: #d63638;';
} else {
gainHtml = data.currency + '0.00';
}
}
// Predicted changes
var imprChange = product.predicted_impressions_change !== null
? (product.predicted_impressions_change > 0 ? '+' : '') + product.predicted_impressions_change.toFixed(1) + '%'
: '-';
var clickChange = product.predicted_clicks_change !== null
? (product.predicted_clicks_change > 0 ? '+' : '') + product.predicted_clicks_change.toFixed(1) + '%'
: '-';
var convChange = product.predicted_conversions_change !== null
? (product.predicted_conversions_change > 0 ? '+' : '') + product.predicted_conversions_change.toFixed(1) + '%'
: '-';
var imprStyle = product.predicted_impressions_change > 0 ? 'color:#00a32a;' : (product.predicted_impressions_change < 0 ? 'color:#d63638;' : '');
var clickStyle = product.predicted_clicks_change > 0 ? 'color:#00a32a;' : (product.predicted_clicks_change < 0 ? 'color:#d63638;' : '');
var convStyle = product.predicted_conversions_change > 0 ? 'color:#00a32a;' : (product.predicted_conversions_change < 0 ? 'color:#d63638;' : '');
// Checkbox and action button
var checkbox = product.has_insight && product.should_update
? '<input type="checkbox" class="informatiq-sp-select-product" data-product-id="' + product.id + '" data-new-price="' + product.suggested_price + '">'
: '';
var actionBtn = product.has_insight && product.should_update
? '<button type="button" class="button button-small informatiq-sp-update-single" data-product-id="' + product.id + '" data-new-price="' + product.suggested_price + '">Update</button>'
: '-';
html += '<tr>';
html += '<td>' + checkbox + '</td>';
html += '<td><a href="post.php?post=' + product.id + '&action=edit" title="SKU: ' + product.sku + '">' + self.truncate(product.name, 40) + '</a></td>';
html += '<td>' + localPrice + priceLabel + '</td>';
html += '<td>' + suggestedPrice + '</td>';
html += '<td style="' + gainStyle + '">' + gainHtml + '</td>';
html += '<td style="' + imprStyle + '">' + imprChange + '</td>';
html += '<td style="' + clickStyle + '">' + clickChange + '</td>';
html += '<td style="' + convStyle + '">' + convChange + '</td>';
html += '<td>' + actionBtn + '</td>';
html += '</tr>';
});
}
$tbody.html(html);
// Render pagination
if (totalPages > 1) {
var paginationHtml = '<span>Page ' + this.currentPage + ' of ' + totalPages + '</span> ';
if (this.currentPage > 1) {
paginationHtml += '<button type="button" class="button button-small informatiq-sp-page" data-page="' + (this.currentPage - 1) + '">&laquo; Prev</button> ';
}
if (this.currentPage < totalPages) {
paginationHtml += '<button type="button" class="button button-small informatiq-sp-page" data-page="' + (this.currentPage + 1) + '">Next &raquo;</button>';
}
$pagination.html(paginationHtml).show();
} else {
$pagination.hide();
}
// Bind events for this page
self.bindComparisonEvents();
},
/**
* Truncate text
*/
truncate: function(str, len) {
if (!str) return '';
return str.length > len ? str.substring(0, len) + '...' : str;
},
/**
* Bind comparison table events
*/
bindComparisonEvents: function() {
var self = this;
// Single update buttons
$('.informatiq-sp-update-single').off('click').on('click', function() {
var $btn = $(this);
var productId = $btn.data('product-id');
var newPrice = $btn.data('new-price');
$btn.prop('disabled', true).text('Updating...');
$.ajax({
url: informatiqSP.ajaxUrl,
type: 'POST',
data: {
action: 'informatiq_sp_update_price',
nonce: informatiqSP.nonce,
product_id: productId,
new_price: newPrice
},
success: function(response) {
if (response.success) {
$btn.text('Done!').addClass('button-primary');
$btn.closest('tr').css('background-color', '#d4edda');
} else {
alert('Error: ' + response.data.message);
$btn.prop('disabled', false).text('Update');
}
},
error: function() {
alert('Request failed');
$btn.prop('disabled', false).text('Update');
}
});
});
// Pagination
$('.informatiq-sp-page').off('click').on('click', function() {
self.currentPage = $(this).data('page');
self.renderComparison();
$('html, body').animate({ scrollTop: $('#informatiq-sp-comparison-results').offset().top - 50 }, 200);
});
// Select all checkboxes
$('#informatiq-sp-select-all, #informatiq-sp-select-all-header').off('change').on('change', function() {
var checked = $(this).prop('checked');
$('.informatiq-sp-select-product').prop('checked', checked);
$('#informatiq-sp-select-all, #informatiq-sp-select-all-header').prop('checked', checked);
self.updateBulkButton();
});
// Individual checkbox
$('.informatiq-sp-select-product').off('change').on('change', function() {
self.updateBulkButton();
});
// Bulk update button
$('#informatiq-sp-bulk-update').off('click').on('click', function() {
self.handleBulkUpdate();
});
},
/**
* Update bulk button state
*/
updateBulkButton: function() {
var count = $('.informatiq-sp-select-product:checked').length;
$('#informatiq-sp-bulk-update').prop('disabled', count === 0).text('Bulk Update Selected (' + count + ')');
},
/**
* Handle bulk update
*/
handleBulkUpdate: function() {
var updates = [];
$('.informatiq-sp-select-product:checked').each(function() {
updates.push({
product_id: $(this).data('product-id'),
new_price: $(this).data('new-price')
});
});
if (updates.length === 0) return;
if (!confirm('Update prices for ' + updates.length + ' products?')) return;
var $btn = $('#informatiq-sp-bulk-update');
$btn.prop('disabled', true).text('Updating...');
$.ajax({
url: informatiqSP.ajaxUrl,
type: 'POST',
data: {
action: 'informatiq_sp_bulk_update_prices',
nonce: informatiqSP.nonce,
updates: updates
},
success: function(response) {
if (response.success) {
alert(response.data.message);
location.reload();
} else {
alert('Error: ' + response.data.message);
$btn.prop('disabled', false).text('Bulk Update Selected');
}
},
error: function() {
alert('Request failed');
$btn.prop('disabled', false).text('Bulk Update Selected');
}
});
},
};
// Initialize when document is ready