Fix remembering family members layer state and refreshing locations

This commit is contained in:
Eugene Burmakin 2025-10-21 19:54:25 +02:00
parent 07216e00dd
commit 05237995cf
7 changed files with 173 additions and 68 deletions

File diff suppressed because one or more lines are too long

View file

@ -13,7 +13,8 @@ class Family::InvitationsController < ApplicationController
end
def show
@invitation = Family::Invitation.find_by!(token: params[:token])
token = params[:token] || params[:id]
@invitation = Family::Invitation.find_by!(token: token)
if @invitation.expired?
redirect_to root_path, alert: 'This invitation has expired.' and return

View file

@ -341,6 +341,11 @@ export default class extends Controller {
mapsController.updateLayerControl({
"Family Members": this.familyMarkersLayer
});
// Dispatch event to notify that Family Members layer is now available
document.dispatchEvent(new CustomEvent('family:layer:ready', {
detail: { layer: this.familyMarkersLayer }
}));
}
setupEventListeners() {

View file

@ -101,6 +101,9 @@ export default class extends BaseController {
this.speedColoredPolylines = this.userSettings.speed_colored_routes || false;
this.speedColorScale = this.userSettings.speed_color_scale || colorFormatEncode(colorStopsFallback);
// Flag to prevent saving layers during initialization/restoration
this.isRestoringLayers = false;
// Ensure we have valid markers array
if (!Array.isArray(this.markers)) {
console.warn('Markers is not an array, setting to empty array');
@ -229,6 +232,9 @@ export default class extends BaseController {
// Initialize layers based on settings
this.initializeLayersFromSettings();
// Listen for Family Members layer becoming ready
this.setupFamilyLayerListener();
// Initialize tracks layer
this.initializeTracksLayer();
@ -465,8 +471,10 @@ export default class extends BaseController {
// Add event listeners for overlay layer changes to keep routes/tracks selector in sync
this.map.on('overlayadd', (event) => {
// Save enabled layers whenever a layer is added
// Save enabled layers whenever a layer is added (unless we're restoring from settings)
if (!this.isRestoringLayers) {
this.saveEnabledLayers();
}
if (event.name === 'Routes') {
this.handleRouteLayerToggle('routes');
@ -523,8 +531,10 @@ export default class extends BaseController {
});
this.map.on('overlayremove', (event) => {
// Save enabled layers whenever a layer is removed
// Save enabled layers whenever a layer is removed (unless we're restoring from settings)
if (!this.isRestoringLayers) {
this.saveEnabledLayers();
}
if (event.name === 'Routes' || event.name === 'Tracks') {
// Don't auto-switch when layers are manually turned off
@ -585,7 +595,8 @@ export default class extends BaseController {
const enabledLayers = [];
const layerNames = [
'Points', 'Routes', 'Tracks', 'Heatmap', 'Fog of War',
'Scratch map', 'Areas', 'Photos', 'Suggested Visits', 'Confirmed Visits'
'Scratch map', 'Areas', 'Photos', 'Suggested Visits', 'Confirmed Visits',
'Family Members'
];
const controlsLayer = {
@ -598,7 +609,8 @@ export default class extends BaseController {
'Areas': this.areasLayer,
'Photos': this.photoMarkers,
'Suggested Visits': this.visitsManager?.getVisitCirclesLayer(),
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer()
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer(),
'Family Members': window.familyMembersController?.familyMarkersLayer
};
layerNames.forEach(name => {
@ -608,16 +620,6 @@ export default class extends BaseController {
}
});
// Add family member layers
if (window.familyController && window.familyController.familyLayers) {
Object.keys(window.familyController.familyLayers).forEach(memberName => {
const layer = window.familyController.familyLayers[memberName];
if (layer && this.map.hasLayer(layer)) {
enabledLayers.push(memberName);
}
});
}
fetch('/api/v1/settings', {
method: 'PATCH',
headers: {
@ -1481,23 +1483,31 @@ export default class extends BaseController {
'Areas': this.areasLayer,
'Photos': this.photoMarkers,
'Suggested Visits': this.visitsManager?.getVisitCirclesLayer(),
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer()
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer(),
'Family Members': window.familyMembersController?.familyMarkersLayer
};
// Add family member layers if available
if (window.familyController && window.familyController.familyLayers) {
Object.entries(window.familyController.familyLayers).forEach(([memberName, layer]) => {
controlsLayer[memberName] = layer;
});
}
// Apply saved layer preferences
Object.entries(controlsLayer).forEach(([name, layer]) => {
if (!layer) return;
if (!layer) {
if (enabledLayers.includes(name)) {
console.log(`Layer ${name} is in enabled layers but layer object is null/undefined`);
}
return;
}
const shouldBeEnabled = enabledLayers.includes(name);
const isCurrentlyEnabled = this.map.hasLayer(layer);
if (name === 'Family Members') {
console.log('Family Members layer check:', {
shouldBeEnabled,
isCurrentlyEnabled,
layerExists: !!layer,
controllerExists: !!window.familyMembersController
});
}
if (shouldBeEnabled && !isCurrentlyEnabled) {
// Add layer to map
layer.addTo(this.map);
@ -1534,6 +1544,11 @@ export default class extends BaseController {
if (this.drawControl && !this.map._controlCorners.topleft.querySelector('.leaflet-draw')) {
this.map.addControl(this.drawControl);
}
} else if (name === 'Family Members') {
// Refresh family locations when layer is restored
if (window.familyMembersController && typeof window.familyMembersController.refreshFamilyLocations === 'function') {
window.familyMembersController.refreshFamilyLocations();
}
}
} else if (!shouldBeEnabled && isCurrentlyEnabled) {
// Remove layer from map
@ -1543,6 +1558,36 @@ export default class extends BaseController {
});
}
setupFamilyLayerListener() {
// Listen for when the Family Members layer becomes available
document.addEventListener('family:layer:ready', (event) => {
console.log('Family layer ready event received');
const enabledLayers = this.userSettings.enabled_map_layers || [];
// Check if Family Members should be enabled based on saved settings
if (enabledLayers.includes('Family Members')) {
const layer = event.detail.layer;
if (layer && !this.map.hasLayer(layer)) {
// Set flag to prevent saving during restoration
this.isRestoringLayers = true;
layer.addTo(this.map);
console.log('Enabled layer: Family Members (from ready event)');
// Refresh family locations
if (window.familyMembersController && typeof window.familyMembersController.refreshFamilyLocations === 'function') {
window.familyMembersController.refreshFamilyLocations();
}
// Reset flag after a short delay to allow all events to complete
setTimeout(() => {
this.isRestoringLayers = false;
}, 100);
}
}
}, { once: true }); // Only listen once
}
toggleRightPanel() {
if (this.rightPanel) {
const panel = document.querySelector('.leaflet-right-panel');

View file

@ -125,32 +125,41 @@ export function showFlashMessage(type, message) {
if (!flashContainer) {
flashContainer = document.createElement('div');
flashContainer.id = 'flash-messages';
// Use z-[9999] to ensure flash messages appear above navbar (z-50)
flashContainer.className = 'fixed top-20 right-5 flex flex-col-reverse gap-2';
flashContainer.style.zIndex = '9999';
flashContainer.className = 'fixed top-5 right-5 flex flex-col gap-2 z-50';
document.body.appendChild(flashContainer);
}
// Create the flash message div
// Create the flash message div with DaisyUI alert classes
const flashDiv = document.createElement('div');
flashDiv.setAttribute('data-controller', 'removals');
flashDiv.className = `flex items-center justify-between ${classesForFlash(type)} py-3 px-5 rounded-lg shadow-lg`;
flashDiv.setAttribute('data-removals-timeout-value', type === 'notice' || type === 'success' ? '5000' : '0');
flashDiv.setAttribute('role', 'alert');
flashDiv.className = `alert ${getAlertClass(type)} shadow-lg z-[6000]`;
// Create the message div
const messageDiv = document.createElement('div');
messageDiv.className = 'mr-4';
messageDiv.innerText = message;
// Create the content wrapper
const contentDiv = document.createElement('div');
contentDiv.className = 'flex items-center gap-2';
// Add the icon
const icon = getFlashIcon(type);
contentDiv.appendChild(icon);
// Create the message span
const messageSpan = document.createElement('span');
messageSpan.innerText = message;
contentDiv.appendChild(messageSpan);
// Create the close button
const closeButton = document.createElement('button');
closeButton.setAttribute('type', 'button');
closeButton.setAttribute('data-action', 'click->removals#remove');
closeButton.className = 'ml-auto'; // Ensures button stays on the right
closeButton.setAttribute('aria-label', 'Close');
closeButton.className = 'btn btn-sm btn-circle btn-ghost';
// Create the SVG icon for the close button
const closeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
closeIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
closeIcon.setAttribute('class', 'h-6 w-6');
closeIcon.setAttribute('class', 'h-5 w-5');
closeIcon.setAttribute('fill', 'none');
closeIcon.setAttribute('viewBox', '0 0 24 24');
closeIcon.setAttribute('stroke', 'currentColor');
@ -164,11 +173,12 @@ export function showFlashMessage(type, message) {
// Append all elements
closeIcon.appendChild(closeIconPath);
closeButton.appendChild(closeIcon);
flashDiv.appendChild(messageDiv);
flashDiv.appendChild(contentDiv);
flashDiv.appendChild(closeButton);
flashContainer.appendChild(flashDiv);
// Automatically remove after 5 seconds
// Automatically remove after 5 seconds for notice/success
if (type === 'notice' || type === 'success') {
setTimeout(() => {
if (flashDiv && flashDiv.parentNode) {
flashDiv.remove();
@ -179,18 +189,59 @@ export function showFlashMessage(type, message) {
}
}, 5000);
}
}
function classesForFlash(type) {
function getAlertClass(type) {
switch (type) {
case 'error':
return 'bg-red-100 text-red-700 border-red-300';
case 'alert':
return 'alert-error';
case 'notice':
return 'bg-blue-100 text-blue-700 border-blue-300';
case 'info':
return 'alert-info';
case 'success':
return 'alert-success';
case 'warning':
return 'alert-warning';
default:
return 'bg-blue-100 text-blue-700 border-blue-300';
return 'alert-info';
}
}
function getFlashIcon(type) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('class', 'h-6 w-6 shrink-0 stroke-current');
svg.setAttribute('fill', 'none');
svg.setAttribute('viewBox', '0 0 24 24');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
path.setAttribute('stroke-width', '2');
switch (type) {
case 'error':
case 'alert':
path.setAttribute('d', 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z');
break;
case 'success':
path.setAttribute('d', 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z');
break;
case 'warning':
path.setAttribute('d', 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z');
break;
case 'notice':
case 'info':
default:
path.setAttribute('d', 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z');
break;
}
svg.appendChild(path);
return svg;
}
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {

View file

@ -17,6 +17,9 @@ class Users::ImportDataJob < ApplicationJob
import_stats = Users::ImportData.new(user, archive_path).import
# Reset counter caches after import completes
User.reset_counters(user.id, :points)
Rails.logger.info "Import completed successfully for user #{user.email}: #{import_stats}"
rescue ActiveRecord::RecordNotFound => e
ExceptionReporter.call(e, "Import job failed for import_id #{import_id} - import not found")

View file

@ -14,8 +14,8 @@
You've been invited by <strong class="text-base-content"><%= @invitation.invited_by.email %></strong> to join their family. Create your account to accept the invitation and start sharing location data.
</p>
<div class="alert alert-info inline-flex rounded-lg px-4 py-2 mt-4">
<%= icon 'info', class: "h-5 w-5 mr-2" %>
<div class="alert alert-info inline-flex items-center rounded-lg px-4 py-3 mt-4 gap-3">
<%= icon 'info', class: "h-5 w-5 shrink-0" %>
<span class="text-sm font-medium">
Your email (<%= @invitation.email %>) will be used for this account
</span>
@ -96,23 +96,23 @@
<!-- Invitation Details -->
<div class="bg-base-300 rounded-lg p-6 mb-6">
<h3 class="text-sm font-medium text-base-content opacity-70 mb-3">Invitation Details</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-base-content opacity-60">Family:</span>
<span class="ml-2 font-semibold text-base-content"><%= @invitation.family.name %></span>
<h3 class="text-lg font-semibold text-base-content mb-6">Invitation Details</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<div class="text-sm text-base-content opacity-60">Family:</div>
<div class="text-base font-semibold text-base-content"><%= @invitation.family.name %></div>
</div>
<div>
<span class="text-base-content opacity-60">Invited by:</span>
<span class="ml-2 font-semibold text-base-content"><%= @invitation.invited_by.email %></span>
<div class="space-y-2">
<div class="text-sm text-base-content opacity-60">Invited by:</div>
<div class="text-base font-semibold text-base-content"><%= @invitation.invited_by.email %></div>
</div>
<div>
<span class="text-base-content opacity-60">Your email:</span>
<span class="ml-2 font-semibold text-base-content"><%= @invitation.email %></span>
<div class="space-y-2">
<div class="text-sm text-base-content opacity-60">Your email:</div>
<div class="text-base font-semibold text-base-content"><%= @invitation.email %></div>
</div>
<div>
<span class="text-base-content opacity-60">Expires:</span>
<span class="ml-2 font-semibold text-base-content"><%= @invitation.expires_at.strftime('%b %d, %Y') %></span>
<div class="space-y-2">
<div class="text-sm text-base-content opacity-60">Expires:</div>
<div class="text-base font-semibold text-base-content"><%= @invitation.expires_at.strftime('%b %d, %Y') %></div>
</div>
</div>
</div>