Development of Pickup Point Selection Widget on Map for 1C-Bitrix
When placing an order, users should select the nearest pickup point — on a map, with filters by opening hours, pickup type, distance from address. The standard sale.order.ajax component offers only a dropdown list with addresses. This works for 10 points, but becomes inconvenient with 500+. A widget with a map solves this problem.
Where the Widget Lives in Bitrix Architecture
The widget is embedded in the order placement process. We use the OnBeforeSaleOrderFinalAction event or the sale.checkout.v2 component template — depending on which checkout is used on the site.
The widget is a separate JS component that:
- Loads the PVZ list via AJAX request to a server handler.
- Renders the map (Yandex.Maps or Leaflet with OpenStreetMap).
- Places PVZ markers on the map.
- Shows PVZ information on marker click.
- On selection confirmation, writes PVZ to order property.
Storing Pickup Point Data
If PVZ comes from a delivery service (CDEK, Boxberry, Russian Post) — data is synchronized periodically. Structure:
CREATE TABLE custom_pvz_points (
id SERIAL PRIMARY KEY,
provider VARCHAR(50) NOT NULL, -- 'cdek', 'boxberry', 'pochta'
external_id VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
address TEXT NOT NULL,
city VARCHAR(100),
lat DECIMAL(10,8) NOT NULL,
lng DECIMAL(11,8) NOT NULL,
schedule JSON,
phone VARCHAR(50),
is_active TINYINT DEFAULT 1,
synced_at DATETIME,
INDEX idx_city (city),
INDEX idx_coords (lat, lng),
UNIQUE KEY uk_provider_ext (provider, external_id)
);
Synchronization is triggered via Bitrix agent (\Bitrix\Main\Agent) once a day or on request from the admin panel.
Server Handler (AJAX Endpoint)
Requests from the widget are handled via /bitrix/services/main/ajax.php or local/ajax/pvz.php. The handler accepts parameters: city, map center coordinates, radius, delivery provider.
// Get PVZ within N km radius from coordinates
$lat = (float)$_REQUEST['lat'];
$lng = (float)$_REQUEST['lng'];
$radius = (int)($_REQUEST['radius'] ?? 10); // km
// Haversine formula in SQL
$sql = "
SELECT *, (
6371 * ACOS(
COS(RADIANS({$lat})) * COS(RADIANS(lat))
* COS(RADIANS(lng) - RADIANS({$lng}))
+ SIN(RADIANS({$lat})) * SIN(RADIANS(lat))
)
) AS distance
FROM custom_pvz_points
WHERE is_active = 1
HAVING distance <= {$radius}
ORDER BY distance
LIMIT 100
";
Result is cached via \Bitrix\Main\Data\Cache with key by city and provider. PVZ list changes once a day — 12–24 hour cache is justified.
Frontend: Integration with Yandex.Maps
// Map initialization
ymaps.ready(function() {
const map = new ymaps.Map('pvz-map', {
center: [userLat, userLng],
zoom: 12,
controls: ['zoomControl', 'geolocationControl']
});
// Clustered markers for large number of PVZ
const clusterer = new ymaps.Clusterer({
preset: 'islands#invertedDarkBlueClusterIcons',
groupByCoordinates: false,
});
points.forEach(pvz => {
const placemark = new ymaps.Placemark(
[pvz.lat, pvz.lng],
{
balloonContentHeader: pvz.name,
balloonContentBody: `${pvz.address}<br>${pvz.schedule}`,
hintContent: pvz.name,
},
{ preset: 'islands#darkBlueIcon' }
);
placemark.events.add('click', function() {
selectPvz(pvz);
});
clusterer.add(placemark);
});
map.geoObjects.add(clusterer);
});
When selecting a PVZ via selectPvz(), hidden order form fields are filled — delivery address and external point ID. This data is passed to order property DELIVERY_LOCATION or custom property UF_PVZ_ID.
User Geolocation
When opening the widget, request user coordinates via navigator.geolocation.getCurrentPosition(). If permission is not granted — determine city by IP (service ip-api.com or built-in Bitrix geolocation \Bitrix\Main\Web\IpTools). Map centers on the found city.
Integration with Delivery Cost Calculation
After selecting a PVZ, the widget updates delivery cost. Request to handler calling \Bitrix\Sale\Delivery\Services\Manager::calculateDelivery() with selected point — returns cost and timeframe. Data is shown next to PVZ address before confirmation.
Synchronization with Providers
| Provider | API / Method | Sync Frequency |
|---|---|---|
| CDEK | GET /v2/deliverypoints |
Once a day |
| Boxberry | ListPoints (SOAP/REST) |
Once a day |
| Russian Post | Tariff.offices |
Once a week |
| DPD | getParcelShops |
Once a day |
| Yandex.Delivery | GET /pickup-points |
Once a day |
Bitrix agent triggers synchronization, updates custom_pvz_points via INSERT ... ON DUPLICATE KEY UPDATE, removes is_active flag from disappeared points.
Timeline
| Stage | Duration |
|---|---|
| Data schema design, cartographic API selection | 1–2 days |
| Server side: storage, AJAX handler, synchronization | 3–5 days |
| Frontend: map, markers, clustering, PVZ selection | 4–6 days |
| Integration into checkout (pass to order) | 2–3 days |
| Cost calculation on PVZ selection | 1–2 days |
| Testing + mobile adaptation | 2–3 days |
Total: 2–3 weeks. If integration with multiple delivery providers simultaneously — add 3–5 days for each.







