diff --git a/app/static/css/style.css b/app/static/css/style.css
index 028394e..d590fcb 100644
--- a/app/static/css/style.css
+++ b/app/static/css/style.css
@@ -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;
}
diff --git a/app/static/js/app.js b/app/static/js/app.js
index 3905228..8df0a5c 100644
--- a/app/static/js/app.js
+++ b/app/static/js/app.js
@@ -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(`${c.name}
${typeName}`);
+
+ 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(`${c.name}`);
- 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();
}
diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js
index b72a27f..278669d 100644
--- a/app/static/js/contacts.js
+++ b/app/static/js/contacts.js
@@ -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();
diff --git a/app/templates/base.html b/app/templates/base.html
index ac0e909..e908c12 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -284,6 +284,36 @@