mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Fix remembering family members layer state and refreshing locations
This commit is contained in:
parent
07216e00dd
commit
05237995cf
7 changed files with 173 additions and 68 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
this.saveEnabledLayers();
|
||||
// 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
|
||||
this.saveEnabledLayers();
|
||||
// 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');
|
||||
|
|
|
|||
|
|
@ -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,33 +173,75 @@ 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
|
||||
setTimeout(() => {
|
||||
if (flashDiv && flashDiv.parentNode) {
|
||||
flashDiv.remove();
|
||||
// Remove container if empty
|
||||
if (flashContainer && !flashContainer.hasChildNodes()) {
|
||||
flashContainer.remove();
|
||||
// Automatically remove after 5 seconds for notice/success
|
||||
if (type === 'notice' || type === 'success') {
|
||||
setTimeout(() => {
|
||||
if (flashDiv && flashDiv.parentNode) {
|
||||
flashDiv.remove();
|
||||
// Remove container if empty
|
||||
if (flashContainer && !flashContainer.hasChildNodes()) {
|
||||
flashContainer.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}, 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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue