#49 feat: detalle de producto en popup de leaflet en el mapa de provenance en lugar de modal web

This commit is contained in:
2026-08-16 14:22:00 -05:00
parent 6966c4180a
commit e286424919
3 changed files with 161 additions and 52 deletions

View File

@@ -3,8 +3,19 @@
<template v-if="productRows.length">
<div class="map-layout">
<v-card class="product-panel" data-test="map-product-box" variant="tonal">
<v-card-title class="text-subtitle-1 font-weight-bold py-2">
Productos ({{ productRows.length }})
<v-card-title class="d-flex align-center justify-space-between py-2">
<span class="text-subtitle-1 font-weight-bold">
Productos ({{ productRows.length }})
</span>
<v-btn
v-if="hasMarkers"
data-test="map-reset-zoom"
density="comfortable"
icon="mdi-fit-to-page"
size="small"
variant="flat"
@click="resetZoom"
/>
</v-card-title>
<v-divider />
<v-list density="compact">
@@ -32,18 +43,7 @@
<div class="map-area">
<template v-if="hasMarkers">
<div class="map-container">
<div ref="mapEl" class="map-wrapper" />
<v-btn
class="map-reset-zoom"
data-test="map-reset-zoom"
density="comfortable"
icon="mdi-fit-to-page"
size="small"
variant="flat"
@click="resetZoom"
/>
</div>
<div ref="mapEl" class="map-wrapper" />
<v-alert
v-if="!storePosition"
class="mt-2"
@@ -53,7 +53,7 @@
La ubicación de la tienda no está configurada, por lo que no se mostrará su recorrido.
</v-alert>
<p class="text-caption text-medium-emphasis mt-2 mb-0">
Pasa el cursor sobre un producto para ver su recorrido hasta la tienda.
Pasa el cursor sobre un producto para ver su recorrido hasta la tienda. Haz clic en un marcador para ver el detalle.
</p>
</template>
<v-alert
@@ -196,7 +196,7 @@
})
}
function buildGroupPopup (items) {
function buildGroupPopup (items, onItemClick) {
const container = document.createElement('div')
container.className = 'provenance-popup-list'
items.forEach((item, index) => {
@@ -215,12 +215,64 @@
name.className = 'provenance-popup-name'
name.textContent = item.product.name
row.appendChild(name)
row.addEventListener('click', () => openDialog(item))
row.addEventListener('click', () => onItemClick(item))
container.appendChild(row)
})
return container
}
function appendEntityRow (container, label, icon, color, entity) {
if (!entity) return
const row = document.createElement('div')
row.className = 'provenance-detail-row'
const iconEl = document.createElement('i')
iconEl.className = `mdi ${icon}`
iconEl.style.color = color
row.appendChild(iconEl)
const body = document.createElement('div')
const labelEl = document.createElement('div')
labelEl.className = 'provenance-detail-label'
labelEl.textContent = label
const nameEl = document.createElement('div')
nameEl.className = 'provenance-detail-name'
nameEl.textContent = entity.name
body.appendChild(labelEl)
body.appendChild(nameEl)
row.appendChild(body)
container.appendChild(row)
}
function buildDetailPopup (markerData) {
const container = document.createElement('div')
container.className = 'provenance-detail-popup'
const header = document.createElement('div')
header.className = 'provenance-detail-header'
const image = markerData.product.catalogue_images?.[0]
if (image) {
const img = document.createElement('img')
img.src = image
img.className = 'provenance-detail-img'
header.appendChild(img)
}
const title = document.createElement('div')
title.className = 'provenance-detail-title'
title.textContent = markerData.product.name
header.appendChild(title)
container.appendChild(header)
const rel = markerData.relation || {}
appendEntityRow(container, 'Proveedor', 'mdi-truck', '#1976d2', rel.supplier)
appendEntityRow(container, 'Organización', 'mdi-domain', '#fb8c00', rel.organization)
appendEntityRow(container, 'Municipio', 'mdi-map-marker', '#4caf50', rel.municipality)
appendEntityRow(container, 'Departamento', 'mdi-map', '#3f51b5', rel.department)
appendEntityRow(container, 'País', 'mdi-earth', '#9c27b0', rel.country)
return container
}
function openProductDetail (marker, markerData) {
marker.setPopupContent(buildDetailPopup(markerData))
marker.openPopup()
}
function showLine (from, to) {
const line = L.polyline([from, to], {
color: '#26a69a',
@@ -251,22 +303,25 @@
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.bindPopup(buildDetailPopup(item))
marker.on('mouseover', () => {
if (storePosition.value) showLine(item.position, storePosition.value)
})
marker.on('mouseout', clearLines)
productMarkers.push({ marker, data: item })
productMarkers.push({ marker, items: [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.bindPopup(buildGroupPopup(group.items, item => openProductDetail(marker, item)))
marker.on('popupclose', () => {
marker.bindPopup(buildGroupPopup(group.items, item => openProductDetail(marker, item)))
})
marker.on('mouseover', () => {
if (storePosition.value) group.items.forEach(item => showLine(item.position, storePosition.value))
})
marker.on('mouseout', clearLines)
productMarkers.push({ marker, data: group.items })
productMarkers.push({ marker, items: group.items })
})
}
@@ -280,6 +335,14 @@
function onRowClick (row) {
if (row.position && map) {
map.flyTo(row.position, Math.max(map.getZoom(), 12), { duration: 0.6 })
const entry = productMarkers.find(entry =>
entry.items.some(item => item.product.id === row.product.id && item.relation === row.relation)
)
if (entry) {
const item = entry.items.find(i => i.relation === row.relation) || entry.items[0]
openProductDetail(entry.marker, item)
return
}
}
selectedProduct.value = row.product
selectedRelation.value = row.relation
@@ -290,12 +353,6 @@
if (map) fitBounds()
}
function openDialog (markerData) {
selectedProduct.value = markerData.product
selectedRelation.value = markerData.relation
modalVisible.value = true
}
function initMap () {
if (!mapEl.value || map) return
const center = storePosition.value || markers.value[0].position
@@ -353,10 +410,6 @@
min-width: 0;
}
.map-container {
position: relative;
}
.map-wrapper {
position: relative;
width: 100%;
@@ -366,13 +419,6 @@
z-index: 0;
}
.map-reset-zoom {
position: absolute;
top: 12px;
right: 12px;
z-index: 1000;
}
@media (max-width: 900px) {
.map-layout {
flex-direction: column;
@@ -468,6 +514,52 @@
font-weight: 500;
}
.map-wrapper :deep(.provenance-detail-popup) {
min-width: 200px;
max-width: 260px;
}
.map-wrapper :deep(.provenance-detail-header) {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.map-wrapper :deep(.provenance-detail-img) {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.map-wrapper :deep(.provenance-detail-title) {
font-weight: 700;
}
.map-wrapper :deep(.provenance-detail-row) {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 4px 0;
}
.map-wrapper :deep(.provenance-detail-row .mdi) {
font-size: 18px;
margin-top: 2px;
}
.map-wrapper :deep(.provenance-detail-label) {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
opacity: 0.7;
}
.map-wrapper :deep(.provenance-detail-name) {
font-weight: 500;
}
.map-wrapper :deep(.leaflet-control-attribution) {
font-size: 10px;
}