feat: Add colored markers and type filtering on contact map

- Use CircleMarker with different colors per contact type:
  - CLI (blue), REP (green), ROOM (purple), SENS (orange)
- Add type filter checkboxes in map modal header
- Dynamically update markers when filters change
- Hide filter panel for single contact view
- Reduce message action buttons to 32x32px

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-16 08:21:11 +01:00
parent f00234af88
commit d395656445
5 changed files with 155 additions and 21 deletions
+3 -3
View File
@@ -723,11 +723,11 @@ main {
/* Icon-only action buttons on message bubbles */
.btn-msg-action {
width: 36px;
height: 36px;
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
font-size: 1rem;
}
+88 -18
View File
@@ -20,6 +20,22 @@ let dmUnreadCounts = {}; // Track unread DM counts per conversation
let leafletMap = null;
let markersGroup = null;
let contactsGeoCache = {}; // { 'contactName': { lat, lon }, ... }
let allContactsWithGps = []; // Cached contacts for map filtering
// Contact type colors for map markers
const CONTACT_TYPE_COLORS = {
1: '#2196F3', // CLI - blue
2: '#4CAF50', // REP - green
3: '#9C27B0', // ROOM - purple
4: '#FF9800' // SENS - orange
};
const CONTACT_TYPE_NAMES = {
1: 'CLI',
2: 'REP',
3: 'ROOM',
4: 'SENS'
};
/**
* Global navigation function - closes offcanvas and cleans up before navigation
@@ -77,6 +93,10 @@ function showContactOnMap(name, lat, lon) {
const modal = new bootstrap.Modal(modalEl);
document.getElementById('mapModalTitle').textContent = name;
// Hide type filter panel for single contact view
const filterPanel = document.getElementById('mapTypeFilter');
if (filterPanel) filterPanel.classList.add('d-none');
const onShown = function() {
initLeafletMap();
markersGroup.clearLayers();
@@ -99,6 +119,60 @@ function showContactOnMap(name, lat, lon) {
// Make showContactOnMap available globally (for contacts.js)
window.showContactOnMap = showContactOnMap;
/**
* Get selected contact types from map filter checkboxes
*/
function getSelectedMapTypes() {
const types = [];
if (document.getElementById('mapFilterCLI')?.checked) types.push(1);
if (document.getElementById('mapFilterREP')?.checked) types.push(2);
if (document.getElementById('mapFilterROOM')?.checked) types.push(3);
if (document.getElementById('mapFilterSENS')?.checked) types.push(4);
return types;
}
/**
* Update map markers based on current filter selection
*/
function updateMapMarkers() {
if (!leafletMap || !markersGroup) return;
markersGroup.clearLayers();
const selectedTypes = getSelectedMapTypes();
const filteredContacts = allContactsWithGps.filter(c => selectedTypes.includes(c.type));
if (filteredContacts.length === 0) {
leafletMap.setView([52.0, 19.0], 6);
return;
}
const bounds = [];
filteredContacts.forEach(c => {
const color = CONTACT_TYPE_COLORS[c.type] || '#2196F3';
const typeName = CONTACT_TYPE_NAMES[c.type] || 'Unknown';
L.circleMarker([c.adv_lat, c.adv_lon], {
radius: 10,
fillColor: color,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
})
.addTo(markersGroup)
.bindPopup(`<b>${c.name}</b><br><span class="text-muted">${typeName}</span>`);
bounds.push([c.adv_lat, c.adv_lon]);
});
if (bounds.length === 1) {
leafletMap.setView(bounds[0], 13);
} else {
leafletMap.fitBounds(bounds, { padding: [20, 20] });
}
}
/**
* Show all contacts with GPS on map
*/
@@ -107,6 +181,10 @@ async function showAllContactsOnMap() {
const modal = new bootstrap.Modal(modalEl);
document.getElementById('mapModalTitle').textContent = 'All Contacts';
// Show type filter panel
const filterPanel = document.getElementById('mapTypeFilter');
if (filterPanel) filterPanel.classList.remove('d-none');
const onShown = async function() {
initLeafletMap();
markersGroup.clearLayers();
@@ -116,27 +194,11 @@ async function showAllContactsOnMap() {
const data = await response.json();
if (data.success && data.contacts) {
const contactsWithGps = data.contacts.filter(c =>
allContactsWithGps = data.contacts.filter(c =>
c.adv_lat && c.adv_lon && (c.adv_lat !== 0 || c.adv_lon !== 0)
);
if (contactsWithGps.length === 0) {
leafletMap.setView([52.0, 19.0], 6);
} else {
const bounds = [];
contactsWithGps.forEach(c => {
L.marker([c.adv_lat, c.adv_lon])
.addTo(markersGroup)
.bindPopup(`<b>${c.name}</b>`);
bounds.push([c.adv_lat, c.adv_lon]);
});
if (bounds.length === 1) {
leafletMap.setView(bounds[0], 13);
} else {
leafletMap.fitBounds(bounds, { padding: [20, 20] });
}
}
updateMapMarkers();
}
} catch (err) {
console.error('Error loading contacts for map:', err);
@@ -146,6 +208,14 @@ async function showAllContactsOnMap() {
modalEl.removeEventListener('shown.bs.modal', onShown);
};
// Setup filter checkbox listeners
['mapFilterCLI', 'mapFilterREP', 'mapFilterROOM', 'mapFilterSENS'].forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.onchange = updateMapMarkers;
}
});
modalEl.addEventListener('shown.bs.modal', onShown);
modal.show();
}
+4
View File
@@ -81,6 +81,10 @@ function showContactOnMap(name, lat, lon) {
const modal = new bootstrap.Modal(modalEl);
document.getElementById('mapModalTitle').textContent = name;
// Hide type filter panel for single contact view
const filterPanel = document.getElementById('mapTypeFilter');
if (filterPanel) filterPanel.classList.add('d-none');
const onShown = function() {
initLeafletMap();
markersGroup.clearLayers();
+30
View File
@@ -284,6 +284,36 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<!-- Type filter (hidden for single contact view) -->
<div id="mapTypeFilter" class="d-none px-3 py-2 border-bottom bg-light">
<div class="d-flex flex-wrap gap-3 align-items-center">
<span class="small text-muted me-1">Show:</span>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterCLI" value="1" checked>
<label class="form-check-label small" for="mapFilterCLI">
<span class="badge" style="background-color: #2196F3;">CLI</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterREP" value="2" checked>
<label class="form-check-label small" for="mapFilterREP">
<span class="badge" style="background-color: #4CAF50;">REP</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterROOM" value="3" checked>
<label class="form-check-label small" for="mapFilterROOM">
<span class="badge" style="background-color: #9C27B0;">ROOM</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterSENS" value="4" checked>
<label class="form-check-label small" for="mapFilterSENS">
<span class="badge" style="background-color: #FF9800;">SENS</span>
</label>
</div>
</div>
</div>
<div id="leafletMap" style="height: 400px; width: 100%;"></div>
</div>
</div>
+30
View File
@@ -424,6 +424,36 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<!-- Type filter (hidden for single contact view) -->
<div id="mapTypeFilter" class="d-none px-3 py-2 border-bottom bg-light">
<div class="d-flex flex-wrap gap-3 align-items-center">
<span class="small text-muted me-1">Show:</span>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterCLI" value="1" checked>
<label class="form-check-label small" for="mapFilterCLI">
<span class="badge" style="background-color: #2196F3;">CLI</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterREP" value="2" checked>
<label class="form-check-label small" for="mapFilterREP">
<span class="badge" style="background-color: #4CAF50;">REP</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterROOM" value="3" checked>
<label class="form-check-label small" for="mapFilterROOM">
<span class="badge" style="background-color: #9C27B0;">ROOM</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterSENS" value="4" checked>
<label class="form-check-label small" for="mapFilterSENS">
<span class="badge" style="background-color: #FF9800;">SENS</span>
</label>
</div>
</div>
</div>
<div id="leafletMap" style="height: 400px; width: 100%;"></div>
</div>
</div>