feat: full eBay pricing UI with search, batch, and results grid
This commit is contained in:
@@ -4,5 +4,210 @@ frappe.pages['ebay-pricing'].on_page_load = function(wrapper) {
|
||||
title: __('eBay Pricing'),
|
||||
single_column: true
|
||||
});
|
||||
$('<div style="padding:2rem;"><h3>eBay Pricing</h3><p>Search and batch price systems from eBay sold listings.</p></div>').appendTo(page.main);
|
||||
|
||||
let $container = $(`
|
||||
<div style="padding: 1rem;">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<label>Search Model</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="ebay-search-input"
|
||||
placeholder="Dell Latitude 5410..." autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" id="ebay-search-btn">
|
||||
<i class="fa fa-search"></i> Search
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
<div class="form-group">
|
||||
<label>Batch Size</label>
|
||||
<select class="form-control" id="ebay-batch-size" style="display:inline-block; width:auto;">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
<button class="btn btn-warning" id="ebay-batch-btn">
|
||||
<i class="fa fa-play"></i> Price Batch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="ebay-results-area">
|
||||
<div class="text-muted text-center" style="padding: 4rem;">
|
||||
<i class="fa fa-search" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||
<p>Search for a model or run batch pricing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).appendTo(page.main);
|
||||
|
||||
$container.find('#ebay-search-btn').on('click', function() {
|
||||
let query = $container.find('#ebay-search-input').val().trim();
|
||||
if (!query) {
|
||||
frappe.msgprint(__('Enter a model to search'));
|
||||
return;
|
||||
}
|
||||
search_ebay(query);
|
||||
});
|
||||
|
||||
$container.find('#ebay-search-input').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
$container.find('#ebay-search-btn').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$container.find('#ebay-batch-btn').on('click', function() {
|
||||
let size = $container.find('#ebay-batch-size').val();
|
||||
run_batch(size);
|
||||
});
|
||||
|
||||
function search_ebay(query) {
|
||||
frappe.call({
|
||||
method: 'westech_r2.api.ebay_pricing.search_model',
|
||||
args: { query: query },
|
||||
freeze: true,
|
||||
freeze_message: __('Searching eBay sold listings...'),
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.results) {
|
||||
render_results(r.message);
|
||||
} else {
|
||||
let msg = (r.message && r.message.message) || __('No results found');
|
||||
frappe.msgprint(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function run_batch(size) {
|
||||
frappe.call({
|
||||
method: 'westech_r2.api.ebay_pricing.run_batch',
|
||||
args: { batch_size: size },
|
||||
freeze: true,
|
||||
freeze_message: __('Running batch pricing...'),
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(__('Batch complete: {0} priced, {1} failed, {2} skipped',
|
||||
[r.message.priced, r.message.failed, r.message.skipped]));
|
||||
load_recent_pricing();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render_results(data) {
|
||||
let $area = $container.find('#ebay-results-area').empty();
|
||||
if (!data.results || !data.results.length) {
|
||||
$area.html(`<div class="text-muted text-center" style="padding: 2rem;">No results</div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<table class="table table-bordered">
|
||||
<thead><tr>
|
||||
<th>Title</th>
|
||||
<th>Price</th>
|
||||
<th>Condition</th>
|
||||
<th>Sold</th>
|
||||
<th>Shipping</th>
|
||||
</tr></thead>
|
||||
<tbody>`;
|
||||
data.results.forEach(item => {
|
||||
html += `<tr>
|
||||
<td>${frappe.utils.escape_html(item.title || '')}</td>
|
||||
<td>$${(item.price || 0).toFixed(2)}</td>
|
||||
<td>${frappe.utils.escape_html(item.condition || '')}</td>
|
||||
<td>${item.sold || ''}</td>
|
||||
<td>${item.shipping || ''}</td>
|
||||
</tr>`;
|
||||
});
|
||||
html += `</tbody></table>`;
|
||||
|
||||
if (data.pricing) {
|
||||
html += `<div class="well">
|
||||
<h4>Pricing Summary</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-3"><strong>Low:</strong> $${data.pricing.price_low}</div>
|
||||
<div class="col-md-3"><strong>High:</strong> $${data.pricing.price_high}</div>
|
||||
<div class="col-md-3"><strong>Average:</strong> $${data.pricing.price_average}</div>
|
||||
<div class="col-md-3"><strong>Median:</strong> $${data.pricing.price_auction}</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1rem;">
|
||||
<div class="col-md-6"><strong>Source:</strong> ${data.pricing.source}</div>
|
||||
<div class="col-md-6"><strong>Samples:</strong> ${data.pricing.sample_count}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
$area.html(html);
|
||||
}
|
||||
|
||||
function load_recent_pricing() {
|
||||
frappe.call({
|
||||
method: 'westech_r2.api.ebay_pricing.get_recent_pricing',
|
||||
args: { limit: 50 },
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
render_pricing_grid(r.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render_pricing_grid(items) {
|
||||
let $area = $container.find('#ebay-results-area');
|
||||
if (!items || !items.length) {
|
||||
$area.html(`<div class="text-muted text-center" style="padding: 2rem;">No pricing data yet</div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<h4>Recent Pricing Results</h4>
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead><tr>
|
||||
<th>Manufacturer</th>
|
||||
<th>Model</th>
|
||||
<th>Status</th>
|
||||
<th>Age</th>
|
||||
<th>Low</th>
|
||||
<th>High</th>
|
||||
<th>Avg</th>
|
||||
<th>Samples</th>
|
||||
<th>Source</th>
|
||||
<th>Last Priced</th>
|
||||
</tr></thead>
|
||||
<tbody>`;
|
||||
|
||||
items.forEach(row => {
|
||||
let status_class = 'badge-needs';
|
||||
if (row.pricing_status === 'Priced') status_class = 'badge-fresh';
|
||||
else if (row.pricing_status === 'Manual Override') status_class = 'badge-fresh';
|
||||
else if (row.pricing_status === 'Expired') status_class = 'badge-expired';
|
||||
else if (row.pricing_status === 'Error') status_class = 'badge-error';
|
||||
|
||||
let age = row.days_since_pricing || 0;
|
||||
let age_badge = age < 90 ? 'badge-fresh' : (age < 120 ? 'badge-aging' : 'badge-expired');
|
||||
|
||||
html += `<tr>
|
||||
<td>${frappe.utils.escape_html(row.manufacturer || '')}</td>
|
||||
<td>${frappe.utils.escape_html(row.model || '')}</td>
|
||||
<td><span class="badge ${status_class}">${row.pricing_status}</span></td>
|
||||
<td><span class="badge ${age_badge}">${age} days</span></td>
|
||||
<td>$${row.price_low || ''}</td>
|
||||
<td>$${row.price_high || ''}</td>
|
||||
<td>$${row.price_average || ''}</td>
|
||||
<td>${row.sample_count || ''}</td>
|
||||
<td>${row.source || ''}</td>
|
||||
<td>${frappe.datetime.str_to_user(row.scraped_at) || ''}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += `</tbody></table>`;
|
||||
$area.html(html);
|
||||
}
|
||||
|
||||
load_recent_pricing();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user