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>
515 lines
16 KiB
JavaScript
515 lines
16 KiB
JavaScript
/**
|
|
* Admin JavaScript for Informatiq Smart Pricing
|
|
*/
|
|
|
|
(function($) {
|
|
'use strict';
|
|
|
|
var InformatiqSP = {
|
|
/**
|
|
* Initialize
|
|
*/
|
|
init: function() {
|
|
this.bindEvents();
|
|
},
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
bindEvents: function() {
|
|
$('#informatiq-sp-manual-sync').on('click', this.handleManualSync);
|
|
$('#informatiq-sp-test-connection').on('click', this.handleTestConnection);
|
|
$('#informatiq-sp-revoke-auth').on('click', this.handleRevokeAuth);
|
|
$('#informatiq-sp-compare-products').on('click', this.handleCompareProducts);
|
|
},
|
|
|
|
/**
|
|
* Handle manual sync button click
|
|
*/
|
|
handleManualSync: function(e) {
|
|
e.preventDefault();
|
|
|
|
var $button = $(this);
|
|
var $status = $('#informatiq-sp-sync-status');
|
|
|
|
// Confirm action
|
|
if (!confirm(informatiqSP.strings.confirmSync || 'Are you sure you want to run a manual price sync? This may take several minutes.')) {
|
|
return;
|
|
}
|
|
|
|
// Disable button and show loading state
|
|
$button.prop('disabled', true).addClass('informatiq-sp-loading');
|
|
|
|
// Show status message
|
|
$status
|
|
.removeClass('notice-success notice-error')
|
|
.addClass('notice-info')
|
|
.html('<p>' + (informatiqSP.strings.syncInProgress || 'Sync in progress...') + '</p>')
|
|
.show();
|
|
|
|
// Make AJAX request
|
|
$.ajax({
|
|
url: informatiqSP.ajaxUrl,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'informatiq_sp_manual_sync',
|
|
nonce: informatiqSP.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$status
|
|
.removeClass('notice-info notice-error')
|
|
.addClass('notice-success')
|
|
.html('<p>' + response.data.message + '</p>');
|
|
|
|
// Reload page after 2 seconds to show updated logs
|
|
setTimeout(function() {
|
|
location.reload();
|
|
}, 2000);
|
|
} else {
|
|
$status
|
|
.removeClass('notice-info notice-success')
|
|
.addClass('notice-error')
|
|
.html('<p>Error: ' + (response.data.message || 'Unknown error') + '</p>');
|
|
}
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
$status
|
|
.removeClass('notice-info notice-success')
|
|
.addClass('notice-error')
|
|
.html('<p>Error: ' + errorThrown + '</p>');
|
|
},
|
|
complete: function() {
|
|
$button.prop('disabled', false).removeClass('informatiq-sp-loading');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Handle test connection button click
|
|
*/
|
|
handleTestConnection: function(e) {
|
|
e.preventDefault();
|
|
|
|
var $button = $(this);
|
|
var $status = $('#informatiq-sp-sync-status');
|
|
|
|
// Disable button and show loading state
|
|
$button.prop('disabled', true).addClass('informatiq-sp-loading');
|
|
|
|
// Show status message
|
|
$status
|
|
.removeClass('notice-success notice-error')
|
|
.addClass('notice-info')
|
|
.html('<p>' + (informatiqSP.strings.testInProgress || 'Testing connection...') + '</p>')
|
|
.show();
|
|
|
|
// Make AJAX request
|
|
$.ajax({
|
|
url: informatiqSP.ajaxUrl,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'informatiq_sp_test_connection',
|
|
nonce: informatiqSP.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$status
|
|
.removeClass('notice-info notice-error')
|
|
.addClass('notice-success')
|
|
.html('<p>' + response.data.message + '</p>');
|
|
} else {
|
|
$status
|
|
.removeClass('notice-info notice-success')
|
|
.addClass('notice-error')
|
|
.html('<p>Error: ' + (response.data.message || 'Unknown error') + '</p>');
|
|
}
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
$status
|
|
.removeClass('notice-info notice-success')
|
|
.addClass('notice-error')
|
|
.html('<p>Error: ' + errorThrown + '</p>');
|
|
},
|
|
complete: function() {
|
|
$button.prop('disabled', false).removeClass('informatiq-sp-loading');
|
|
|
|
// Hide status message after 5 seconds
|
|
setTimeout(function() {
|
|
$status.fadeOut();
|
|
}, 5000);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Handle revoke authorization button click
|
|
*/
|
|
handleRevokeAuth: function(e) {
|
|
e.preventDefault();
|
|
|
|
var $button = $(this);
|
|
|
|
// Confirm action
|
|
if (!confirm(informatiqSP.strings.revokeConfirm || 'Are you sure you want to revoke Google authorization?')) {
|
|
return;
|
|
}
|
|
|
|
// Disable button and show loading state
|
|
$button.prop('disabled', true).text(informatiqSP.strings.revokeInProgress || 'Revoking...');
|
|
|
|
// Make AJAX request
|
|
$.ajax({
|
|
url: informatiqSP.ajaxUrl,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'informatiq_sp_revoke_auth',
|
|
nonce: informatiqSP.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
// Reload page to show updated status
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + (response.data.message || 'Unknown error'));
|
|
$button.prop('disabled', false).text('Revoke Authorization');
|
|
}
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
alert('Error: ' + errorThrown);
|
|
$button.prop('disabled', false).text('Revoke Authorization');
|
|
}
|
|
});
|
|
},
|
|
|
|
// Store comparison data for pagination and bulk updates.
|
|
comparisonData: null,
|
|
currentPage: 1,
|
|
perPage: 50,
|
|
|
|
/**
|
|
* Handle compare products button click
|
|
*/
|
|
handleCompareProducts: function(e) {
|
|
e.preventDefault();
|
|
|
|
var self = InformatiqSP;
|
|
var $button = $(this);
|
|
var $spinner = $button.next('.spinner');
|
|
var $results = $('#informatiq-sp-comparison-results');
|
|
var $status = $('#informatiq-sp-sync-status');
|
|
|
|
// Show loading status
|
|
$status
|
|
.removeClass('notice-success notice-error')
|
|
.addClass('notice-info')
|
|
.html('<p><strong>Loading price insights from Google...</strong> This may take a moment.</p>')
|
|
.show();
|
|
|
|
$button.prop('disabled', true);
|
|
$spinner.addClass('is-active');
|
|
|
|
$.ajax({
|
|
url: informatiqSP.ajaxUrl,
|
|
type: 'POST',
|
|
timeout: 180000, // 3 minute timeout
|
|
data: {
|
|
action: 'informatiq_sp_compare_products',
|
|
nonce: informatiqSP.nonce
|
|
},
|
|
success: function(response) {
|
|
console.log('Price insights response:', response);
|
|
$status.hide();
|
|
|
|
if (response.success) {
|
|
self.comparisonData = response.data;
|
|
self.currentPage = 1;
|
|
self.renderComparison();
|
|
$results.show();
|
|
} else {
|
|
$status
|
|
.removeClass('notice-info notice-success')
|
|
.addClass('notice-error')
|
|
.html('<p>Error: ' + (response.data.message || 'Unknown error') + '</p>')
|
|
.show();
|
|
}
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
console.error('AJAX error:', textStatus, errorThrown);
|
|
$status
|
|
.removeClass('notice-info notice-success')
|
|
.addClass('notice-error')
|
|
.html('<p>Error: ' + errorThrown + '</p>')
|
|
.show();
|
|
},
|
|
complete: function() {
|
|
$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) + '">« Prev</button> ';
|
|
}
|
|
if (this.currentPage < totalPages) {
|
|
paginationHtml += '<button type="button" class="button button-small informatiq-sp-page" data-page="' + (this.currentPage + 1) + '">Next »</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
|
|
$(document).ready(function() {
|
|
InformatiqSP.init();
|
|
});
|
|
|
|
})(jQuery);
|