#49 feat: agrupacion de marcadores del mismo punto con contador y popup de productos en el mapa de provenance

This commit is contained in:
2026-08-16 12:24:49 -05:00
parent 6a833a1ea4
commit f24e7ab2ec
3 changed files with 180 additions and 22 deletions

View File

@@ -138,6 +138,16 @@
const hasMarkers = computed(() => markers.value.length > 0)
const markerGroups = computed(() => {
const groups = new Map()
for (const markerData of markers.value) {
const key = markerData.position.join(',')
if (!groups.has(key)) groups.set(key, [])
groups.get(key).push(markerData)
}
return [...groups.values()].map(items => ({ position: items[0].position, items }))
})
const personIcon = L.divIcon({
className: 'provenance-store-pin',
html: '<i class="mdi mdi-account-circle" style="font-size:46px;line-height:1;color:#f44336;"></i>',
@@ -158,6 +168,40 @@
})
}
function groupIcon (group) {
return L.divIcon({
className: 'provenance-group-pin',
html: `<span class="provenance-group-count">${group.items.length}</span><i class="mdi mdi-package-variant"></i>`,
iconSize: [38, 38],
iconAnchor: [19, 38],
})
}
function buildGroupPopup (items) {
const container = document.createElement('div')
container.className = 'provenance-popup-list'
items.forEach((item, index) => {
const row = document.createElement('div')
row.className = 'provenance-popup-item'
row.setAttribute('role', 'button')
row.setAttribute('data-test', `map-popup-item-${index}`)
const image = item.product.catalogue_images?.[0]
if (image) {
const img = document.createElement('img')
img.src = image
img.className = 'provenance-popup-img'
row.appendChild(img)
}
const name = document.createElement('span')
name.className = 'provenance-popup-name'
name.textContent = item.product.name
row.appendChild(name)
row.addEventListener('click', () => openDialog(item))
container.appendChild(row)
})
return container
}
function showLine (from, to) {
const line = L.polyline([from, to], {
color: '#26a69a',
@@ -177,21 +221,33 @@
storeMarker = L.marker(storePosition.value, { icon: personIcon }).addTo(map)
storeMarker.bindTooltip('Tienda')
storeMarker.on('mouseover', () => {
productMarkers.forEach(item => showLine(item.marker.getLatLng(), storePosition.value))
markers.value.forEach(markerData => showLine(markerData.position, storePosition.value))
})
storeMarker.on('mouseout', clearLines)
}
function addProductMarkers () {
markers.value.forEach(markerData => {
const marker = L.marker(markerData.position, { icon: productIcon(markerData) }).addTo(map)
marker.bindTooltip(markerData.product.name)
marker.on('click', () => openDialog(markerData))
markerGroups.value.forEach(group => {
if (group.items.length === 1) {
const item = group.items[0]
const marker = L.marker(item.position, { icon: productIcon(item) }).addTo(map)
marker.bindTooltip(item.product.name)
marker.on('click', () => openDialog(item))
marker.on('mouseover', () => {
if (storePosition.value) showLine(item.position, storePosition.value)
})
marker.on('mouseout', clearLines)
productMarkers.push({ marker, data: item })
return
}
const marker = L.marker(group.position, { icon: groupIcon(group) }).addTo(map)
marker.bindTooltip(`${group.items.length} productos en este punto`)
marker.bindPopup(buildGroupPopup(group.items))
marker.on('mouseover', () => {
if (storePosition.value) showLine(marker.getLatLng(), storePosition.value)
if (storePosition.value) group.items.forEach(item => showLine(item.position, storePosition.value))
})
marker.on('mouseout', clearLines)
productMarkers.push({ marker, data: markerData })
productMarkers.push({ marker, data: group.items })
})
}
@@ -302,6 +358,54 @@
background: transparent;
}
.map-wrapper :deep(.provenance-group-pin) {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #26a69a;
border: 2px solid #ffffff;
border-radius: 50%;
color: #ffffff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
cursor: pointer;
}
.map-wrapper :deep(.provenance-group-pin .provenance-group-count) {
font-weight: 700;
font-size: 14px;
line-height: 1;
}
.map-wrapper :deep(.provenance-group-pin .mdi) {
font-size: 12px;
line-height: 1;
margin-top: 2px;
}
.map-wrapper :deep(.provenance-popup-item) {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
cursor: pointer;
}
.map-wrapper :deep(.provenance-popup-item:hover) {
text-decoration: underline;
}
.map-wrapper :deep(.provenance-popup-img) {
width: 28px;
height: 28px;
border-radius: 4px;
object-fit: cover;
}
.map-wrapper :deep(.provenance-popup-name) {
font-weight: 500;
}
.map-wrapper :deep(.leaflet-control-attribution) {
font-size: 10px;
}