Configuring a Competitor Price Comparison Dashboard in 1C-Bitrix
A category manager should not have to open each individual product card just to spot a pricing problem. A dashboard is a consolidated screen that shows in 30 seconds which products are losing on price, by how much, how many of them there are, and allows a decision to be made right there. The goal of the dashboard is to reduce the time from problem detection to action.
Data Sources
The dashboard is built on three tables:
-
bl_competitor_prices— current competitor prices -
bl_product_price_position— aggregates (competitor min/max/avg, our rank) -
b_catalog_price— our current prices
Data in the aggregate table is updated by an agent after each competitor price synchronization.
Key Dashboard Metrics
Price position — distribution of products by rank:
SELECT rank, COUNT(*) as product_count
FROM bl_product_price_position
WHERE updated_at > NOW() - INTERVAL '24 hours'
GROUP BY rank
ORDER BY rank;
Displayed as a bar chart: "1st place — 34 products, 2nd place — 87, 3rd place — 124..."
Products where we are more expensive than the competitor minimum:
SELECT
ie.ID,
ie.NAME,
ppp.our_price,
ppp.min_comp as competitor_min,
ROUND((ppp.our_price - ppp.min_comp) / ppp.min_comp * 100, 1) as diff_pct,
ppp.rank
FROM bl_product_price_position ppp
JOIN b_iblock_element ie ON ie.ID = ppp.product_id
WHERE ppp.our_price > ppp.min_comp
AND ppp.min_comp > 0
ORDER BY diff_pct DESC
LIMIT 50;
Estimated lost revenue:
SELECT
SUM(
(ppp.our_price - ppp.min_comp) / ppp.our_price * oe.order_count * ppp.our_price
) as estimated_lost_revenue
FROM bl_product_price_position ppp
JOIN (
SELECT product_id, COUNT(DISTINCT order_id) as order_count
FROM b_sale_basket
WHERE date_insert > NOW() - INTERVAL '30 days'
GROUP BY product_id
) oe ON oe.product_id = ppp.product_id
WHERE ppp.our_price > ppp.min_comp;
Dashboard Page Structure
The page at /bitrix/admin/price_dashboard.php consists of several blocks:
Top block — summary KPIs:
- Total products under monitoring: N
- Of those, more expensive than competitors: N (XX%)
- Average price position: X.X
- Products in 1st place: N
Middle block — heatmap by catalog section:
// Aggregates by section
$sectionStats = \Bitrix\Main\Application::getConnection()->query("
SELECT
s.NAME as section_name,
COUNT(*) as total_products,
COUNT(CASE WHEN ppp.rank = 1 THEN 1 END) as on_first_place,
ROUND(AVG(ppp.rank), 1) as avg_rank,
COUNT(CASE WHEN ppp.our_price > ppp.min_comp THEN 1 END) as losing_count
FROM bl_product_price_position ppp
JOIN b_iblock_element ie ON ie.ID = ppp.product_id
JOIN b_iblock_section s ON s.ID = ie.IBLOCK_SECTION_ID
GROUP BY s.ID, s.NAME
ORDER BY losing_count DESC
")->fetchAll();
Each section is a row with color coding: green (>70% of products in 1st place), yellow (50–70%), red (<50%).
Bottom block — problem products table with action buttons:
| Product | SKU | Our price | Competitor min | Difference | Rank | [Change price] |
|---|
The "Change price" button enables inline editing with AJAX save to b_catalog_price. On save, bl_price_change_log records: who changed, when, from which price, to which price.
AJAX Data Refresh
The dashboard updates without a page reload — the "Refresh data" button triggers synchronization with the price source:
document.getElementById('refresh-btn').addEventListener('click', function() {
this.disabled = true;
fetch('/bitrix/services/main/ajax.php?action=PriceDashboard:refresh', {
method: 'POST',
headers: {'X-Bitrix-Csrf-Token': BX.bitrix_sessid()}
})
.then(r => r.json())
.then(data => {
if (data.status === 'ok') location.reload();
});
});
Excel Export
The "Export to Excel" button generates a report via \PhpOffice\PhpSpreadsheet: all products with competitor prices in separate columns (one column per competitor), our price, rank, and recommended price (if the repricer is configured).
Timeline
| Phase | Duration |
|---|---|
| Aggregate queries and optimization | 2 days |
| Top KPI block + chart.js | 1 day |
| Section heatmap | 1 day |
| Problem products table + inline editing | 2 days |
| Excel export | 1 day |
| Testing | 1 day |
| Total | 8–9 days |







