Cut updates. Still need to test the assignment system.

This commit is contained in:
admin 2025-09-08 11:23:27 -06:00
parent 459cea0c3b
commit bc08f0b55d
9 changed files with 747 additions and 160 deletions

View File

@ -759,104 +759,109 @@
<p>Create and manage polygon overlays for the map. Cuts can be used to define areas like wards, neighborhoods, or custom regions.</p> <p>Create and manage polygon overlays for the map. Cuts can be used to define areas like wards, neighborhoods, or custom regions.</p>
<div class="cuts-container"> <div class="cuts-container">
<!-- Map and Drawing Controls --> <!-- Top Section with Existing Cuts and Form -->
<div class="cuts-map-section"> <div class="cuts-top-section">
<div id="cuts-map" class="admin-map"></div> <!-- Existing Cuts List - Moved to Top -->
<div class="cuts-list-section">
<!-- Drawing Toolbar --> <div class="cuts-management-panel">
<div id="cut-drawing-toolbar" class="cut-drawing-toolbar"> <div class="panel-header">
<div class="toolbar-content"> <h3 class="panel-title">Existing Cuts</h3>
<div class="vertex-count" id="vertex-count">0</div> <div class="panel-actions">
<button id="refresh-cuts-btn" class="btn btn-secondary btn-sm">Refresh</button>
<div class="style-controls"> <button id="export-cuts-btn" class="btn btn-secondary btn-sm">Export All</button>
<div class="color-control"> <label for="import-cuts-file" class="btn btn-secondary btn-sm" style="margin: 0;">
<label>Color:</label> Import
<input type="color" id="toolbar-color" value="#3388ff"> <input type="file" id="import-cuts-file" accept=".json" style="display: none;">
</div> </label>
<div class="opacity-control">
<label>Opacity:</label>
<input type="range" id="toolbar-opacity" min="0" max="1" step="0.05" value="0.3">
<span class="opacity-value" id="toolbar-opacity-display">30%</span>
</div> </div>
</div> </div>
<div class="panel-content">
<div class="toolbar-buttons"> <div class="cuts-filters">
<button type="button" id="finish-cut-btn" class="primary" disabled>Finish</button> <input type="text" id="cuts-search" placeholder="Search cuts..." class="form-control">
<button type="button" id="undo-vertex-btn" class="secondary" disabled>Undo</button> <select id="cuts-category-filter" class="form-control">
<button type="button" id="clear-vertices-btn" class="secondary" disabled>Clear</button> <option value="">All Categories</option>
<button type="button" id="cancel-cut-btn" class="danger">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Cut Form -->
<div class="cuts-form-section">
<div class="cuts-management-panel">
<div class="panel-header">
<h3 class="panel-title" id="cut-form-title">Cut Properties</h3>
<div class="panel-actions">
<button id="start-drawing-btn" class="btn btn-primary btn-sm">Start Drawing</button>
</div>
</div>
<div class="panel-content">
<form id="cut-form" class="cut-form">
<!-- Hidden fields moved to prevent duplicates -->
<input type="hidden" id="cut-id" name="id">
<input type="hidden" id="cut-geojson" name="geojson">
<input type="hidden" id="cut-bounds" name="bounds">
<div class="form-group">
<label for="cut-name">Name *</label>
<input type="text" id="cut-name" name="name" required>
</div>
<div class="form-group">
<label for="cut-description">Description</label>
<textarea id="cut-description" name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="cut-category">Category</label>
<select id="cut-category" name="category">
<option value="Custom">Custom</option> <option value="Custom">Custom</option>
<option value="Ward">Ward</option> <option value="Ward">Ward</option>
<option value="Neighborhood">Neighborhood</option> <option value="Neighborhood">Neighborhood</option>
<option value="District">District</option> <option value="District">District</option>
</select> </select>
</div> </div>
<div id="cuts-list" class="cuts-list">
<div class="form-group checkbox-group"> <!-- Cuts will be populated here -->
<input type="checkbox" id="cut-public" name="is_public" checked>
<label for="cut-public">Make this cut visible on the public map</label>
</div> </div>
</div>
<div class="form-group checkbox-group"> </div>
<input type="checkbox" id="cut-official" name="is_official"> </div>
<label for="cut-official">Mark as official cut</label>
<!-- Cut Properties - Streamlined Card -->
<div class="cuts-form-section">
<div class="cuts-management-panel cuts-form-card">
<div class="panel-header">
<h3 class="panel-title" id="cut-form-title">Cut Properties</h3>
<div class="panel-actions">
<button id="start-drawing-btn" class="btn btn-primary btn-sm">Start Drawing</button>
</div> </div>
</div>
<div class="form-actions"> <div class="panel-content">
<button type="submit" id="save-cut-btn" class="btn btn-success" disabled>Save Cut</button> <form id="cut-form" class="cut-form cut-form-compact">
<button type="button" id="reset-form-btn" class="btn btn-secondary">Reset</button> <!-- Hidden fields moved to prevent duplicates -->
<button type="button" id="cancel-edit-btn" class="btn btn-secondary" style="display: none;">Cancel Edit</button> <input type="hidden" id="cut-id" name="id">
</div> <input type="hidden" id="cut-geojson" name="geojson">
</form> <input type="hidden" id="cut-bounds" name="bounds">
<div class="form-row">
<div class="form-group">
<label for="cut-name">Name *</label>
<input type="text" id="cut-name" name="name" required>
</div>
<div class="form-group">
<label for="cut-category">Category</label>
<select id="cut-category" name="category">
<option value="Custom">Custom</option>
<option value="Ward">Ward</option>
<option value="Neighborhood">Neighborhood</option>
<option value="District">District</option>
</select>
</div>
</div>
<div class="form-group">
<label for="cut-description">Description</label>
<textarea id="cut-description" name="description" rows="2"></textarea>
</div>
<div class="form-options">
<div class="form-group checkbox-group">
<input type="checkbox" id="cut-public" name="is_public" checked>
<label for="cut-public">Visible on public map</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="cut-official" name="is_official">
<label for="cut-official">Official cut</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="show-locations-on-map" name="show_locations">
<label for="show-locations-on-map">Show locations on map</label>
</div>
</div>
<div class="form-actions">
<button type="submit" id="save-cut-btn" class="btn btn-success" disabled>Save Cut</button>
<button type="button" id="reset-form-btn" class="btn btn-secondary">Reset</button>
<button type="button" id="cancel-edit-btn" class="btn btn-secondary" style="display: none;">Cancel Edit</button>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Cut Location Management --> <!-- Cut Location Management - Moved Above Map -->
<div class="cuts-location-section" id="cut-location-management" style="display: none;"> <div class="cuts-location-section" id="cut-location-management" style="display: none;">
<div class="cuts-management-panel"> <div class="cuts-management-panel">
<div class="panel-header">
<h3 class="panel-title">Location Management</h3>
<div class="panel-actions">
<button id="toggle-location-visibility" class="btn btn-primary btn-sm">Show Locations</button>
<button id="export-cut-locations" class="btn btn-success btn-sm">Export Data</button>
<button id="print-cut-view" class="btn btn-secondary btn-sm">Print View</button>
</div>
</div>
<div class="panel-content"> <div class="panel-content">
<!-- Location Filters --> <!-- Location Filters -->
<div class="location-filters"> <div class="location-filters">
@ -971,36 +976,45 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div> <!-- Location Management Footer Actions -->
<div class="panel-footer">
<!-- Cuts List --> <h3 class="panel-title">Location Management</h3>
<div class="cuts-list-section">
<div class="cuts-management-panel">
<div class="panel-header">
<h3 class="panel-title">Existing Cuts</h3>
<div class="panel-actions"> <div class="panel-actions">
<button id="refresh-cuts-btn" class="btn btn-secondary btn-sm">Refresh</button> <button id="toggle-location-visibility" class="btn btn-primary btn-sm">Show Locations</button>
<button id="export-cuts-btn" class="btn btn-secondary btn-sm">Export All</button> <button id="export-cut-locations" class="btn btn-success btn-sm">Export Data</button>
<label for="import-cuts-file" class="btn btn-secondary btn-sm" style="margin: 0;"> <button id="print-cut-view" class="btn btn-secondary btn-sm">Print View</button>
Import
<input type="file" id="import-cuts-file" accept=".json" style="display: none;">
</label>
</div> </div>
</div> </div>
<div class="panel-content"> </div>
<div class="cuts-filters"> </div>
<input type="text" id="cuts-search" placeholder="Search cuts..." class="form-control">
<select id="cuts-category-filter" class="form-control"> <!-- Map and Drawing Controls -->
<option value="">All Categories</option> <div class="cuts-map-section">
<option value="Custom">Custom</option> <div id="cuts-map" class="admin-map"></div>
<option value="Ward">Ward</option>
<option value="Neighborhood">Neighborhood</option> <!-- Drawing Toolbar -->
<option value="District">District</option> <div id="cut-drawing-toolbar" class="cut-drawing-toolbar">
</select> <div class="toolbar-content">
<div class="vertex-count" id="vertex-count">0</div>
<div class="style-controls">
<div class="color-control">
<label>Color:</label>
<input type="color" id="toolbar-color" value="#3388ff">
</div>
<div class="opacity-control">
<label>Opacity:</label>
<input type="range" id="toolbar-opacity" min="0" max="1" step="0.05" value="0.3">
<span class="opacity-value" id="toolbar-opacity-display">30%</span>
</div>
</div> </div>
<div id="cuts-list" class="cuts-list">
<!-- Cuts will be populated here --> <div class="toolbar-buttons">
<button type="button" id="finish-cut-btn" class="primary" disabled>Finish</button>
<button type="button" id="undo-vertex-btn" class="secondary" disabled>Undo</button>
<button type="button" id="clear-vertices-btn" class="secondary" disabled>Clear</button>
<button type="button" id="cancel-cut-btn" class="danger">Cancel</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,6 +8,63 @@
gap: var(--padding-base); gap: var(--padding-base);
} }
/* Top section containing form and existing cuts */
.cuts-top-section {
display: flex;
flex-direction: column;
gap: var(--padding-base);
}
/* Streamlined form card for Cut Properties */
.cuts-form-card {
margin-bottom: 0;
}
.cuts-form-card .panel-content {
padding: 15px 20px;
}
.cut-form-compact {
display: grid;
gap: 12px;
max-width: none;
}
.cut-form-compact .form-row {
display: grid;
grid-template-columns: 1fr 200px;
gap: 15px;
}
.cut-form-compact .form-options {
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.cut-form-compact .checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.cut-form-compact .checkbox-group input {
margin: 0;
}
.cut-form-compact .checkbox-group label {
margin: 0;
font-weight: normal;
font-size: 14px;
}
.cut-form-compact textarea {
resize: vertical;
min-height: 60px;
}
.cuts-map-section { .cuts-map-section {
position: relative; position: relative;
height: 500px; height: 500px;
@ -24,8 +81,9 @@
} }
.cuts-form-section, .cuts-form-section,
.cuts-list-section { .cuts-list-section,
margin-top: var(--padding-base); .cuts-location-section {
margin-top: 0;
} }
.cuts-filters { .cuts-filters {
@ -42,6 +100,122 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
/* Location popup styling for cuts map */
.location-popup {
font-size: 13px;
line-height: 1.4;
max-width: 280px;
}
.location-popup .location-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #eee;
}
.location-popup .location-header strong {
color: #333;
font-size: 14px;
}
.location-popup .support-level {
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.location-popup .support-level-1 {
background: #28a745;
color: white;
}
.location-popup .support-level-2 {
background: #ffc107;
color: #212529;
}
.location-popup .support-level-3 {
background: #fd7e14;
color: white;
}
.location-popup .support-level-4 {
background: #dc3545;
color: white;
}
.location-popup .support-level-unknown {
background: #6c757d;
color: white;
}
.location-popup .location-address {
margin-bottom: 6px;
color: #555;
font-weight: 500;
}
.location-popup .location-contact {
margin-bottom: 8px;
color: #666;
font-size: 12px;
}
.location-popup .location-details {
margin-bottom: 8px;
}
.location-popup .detail-row {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
font-size: 12px;
}
.location-popup .detail-label {
color: #666;
font-weight: 500;
}
.location-popup .detail-value {
color: #333;
}
.location-popup .detail-value.coordinates {
font-family: 'Courier New', monospace;
font-size: 11px;
}
.location-popup .location-notes {
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
}
.location-popup .location-notes strong {
color: #333;
}
/* Popup container styling */
.leaflet-popup.location-popup-container .leaflet-popup-content {
margin: 12px 16px;
}
.leaflet-popup.location-popup-container .leaflet-popup-content-wrapper {
border-radius: 6px;
}
.leaflet-popup.location-popup-container .leaflet-popup-tip {
background: white;
}
/* Disabled Form State */ /* Disabled Form State */
.cut-form.disabled { .cut-form.disabled {
opacity: 0.7; opacity: 0.7;
@ -196,23 +370,37 @@
/* Large Screen Layout for Cuts */ /* Large Screen Layout for Cuts */
@media (min-width: 1200px) { @media (min-width: 1200px) {
.cuts-container { .cuts-top-section {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 2fr 1fr;
gap: var(--padding-base); gap: var(--padding-base);
} }
.cuts-form-section,
.cuts-list-section { .cuts-list-section {
display: grid; order: 1;
grid-template-columns: 1fr 1fr;
gap: var(--padding-base);
margin-top: 0;
} }
.cuts-form-section > *, .cuts-form-section {
.cuts-list-section > * { order: 2;
grid-column: span 1; }
}
/* Medium and Small Screen Adjustments */
@media (max-width: 768px) {
.cut-form-compact .form-row {
grid-template-columns: 1fr;
gap: 12px;
}
.cut-form-compact .form-options {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.cuts-filters {
grid-template-columns: 1fr;
gap: 8px;
} }
} }
@ -269,13 +457,46 @@
/* Input group for public links */ /* Input group for public links */
.input-group { .input-group {
display: flex; display: flex;
gap: 0.5rem; border-radius: var(--border-radius);
} }
.input-group input { .input-group input {
flex: 1; flex: 1;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
} }
.input-group .btn { .input-group .btn {
white-space: nowrap; border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Panel footer styling - same as header but positioned at bottom */
.panel-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: none;
border-top: 1px solid var(--border-color);
background: #e3f2fd;
background: linear-gradient(to bottom, #e3f2fd, #bbdefb);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
margin: 0;
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.05);
border-top: 2px solid #90caf9;
}
.panel-footer h3 {
margin: 0;
font-size: 1.1rem;
color: var(--text-primary);
font-weight: 600;
}
.panel-footer .panel-actions {
display: flex;
gap: 10px;
} }

View File

@ -1185,7 +1185,7 @@
} }
.cuts-location-section .panel-title { .cuts-location-section .panel-title {
color: white; color: rgb(0, 0, 0);
} }
.cuts-location-section .btn-sm { .cuts-location-section .btn-sm {

View File

@ -5,7 +5,8 @@
// Global admin state // Global admin state
let adminAppState = { let adminAppState = {
currentSection: 'dashboard' currentSection: 'dashboard',
dashboardLoading: false
}; };
// Utility function to create a local date from YYYY-MM-DD string // Utility function to create a local date from YYYY-MM-DD string
@ -173,9 +174,8 @@ function loadSectionData(sectionId) {
} }
break; break;
case 'dashboard': case 'dashboard':
if (typeof loadDashboardData === 'function') { // Always use our guarded dashboard loading function
loadDashboardData(); loadDashboardDataFromDashboardModule();
}
break; break;
case 'shifts': case 'shifts':
if (typeof loadAdminShifts === 'function') { if (typeof loadAdminShifts === 'function') {
@ -188,6 +188,21 @@ function loadSectionData(sectionId) {
loadUsers(); loadUsers();
} }
break; break;
case 'start-location':
// Initialize or refresh map when start location section becomes visible
setTimeout(() => {
if (window.adminMap && window.adminMap.initializeAdminMap) {
console.log('Ensuring admin map is initialized for start-location section...');
// This will either initialize the map (if not done) or invalidate size (if already initialized)
window.adminMap.initializeAdminMap();
// Also load the current start location data
if (window.adminMap.loadCurrentStartLocation) {
window.adminMap.loadCurrentStartLocation();
}
}
}, 100);
break;
case 'convert-data': case 'convert-data':
// Initialize data convert event listeners when section is shown // Initialize data convert event listeners when section is shown
setTimeout(() => { setTimeout(() => {
@ -289,6 +304,71 @@ function debounce(func, wait) {
}; };
} }
// Fallback function to load dashboard data directly from dashboard.js module
async function loadDashboardDataFromDashboardModule() {
// Prevent multiple simultaneous loads
if (adminAppState.dashboardLoading) {
console.log('Dashboard already loading, skipping duplicate request');
return;
}
adminAppState.dashboardLoading = true;
console.log('Loading dashboard data from dashboard module');
try {
// Show loading state
const cards = document.querySelectorAll('.card-value');
cards.forEach(card => {
card.textContent = '...';
card.style.opacity = '0.6';
});
const response = await fetch('/api/admin/dashboard/stats');
const result = await response.json();
if (result.success) {
// Update summary cards
const data = result.data;
document.getElementById('total-locations').textContent = data.totalLocations.toLocaleString();
document.getElementById('overall-score').textContent = data.overallScore;
document.getElementById('sign-delivered').textContent = data.signDelivered.toLocaleString();
document.getElementById('total-users').textContent = data.totalUsers.toLocaleString();
// Clear loading state
cards.forEach(card => {
card.style.opacity = '1';
});
// Create charts if functions are available globally
if (typeof createSupportLevelChart === 'function') {
createSupportLevelChart(data.supportLevels);
}
if (typeof createSignSizesChart === 'function') {
createSignSizesChart(data.signSizes);
}
if (typeof createEntriesChart === 'function') {
createEntriesChart(data.dailyEntries);
}
console.log('Dashboard data loaded successfully');
} else {
showStatus('Failed to load dashboard data', 'error');
}
} catch (error) {
console.error('Dashboard loading error:', error);
showStatus('Error loading dashboard', 'error');
// Clear loading state
const cards = document.querySelectorAll('.card-value');
cards.forEach(card => {
card.style.opacity = '1';
card.textContent = 'Error';
});
} finally {
adminAppState.dashboardLoading = false;
}
}
// Initialize the admin core when DOM is loaded // Initialize the admin core when DOM is loaded
function initializeAdminCore() { function initializeAdminCore() {
// Set initial viewport dimensions and listen for resize events // Set initial viewport dimensions and listen for resize events
@ -324,5 +404,6 @@ window.adminCore = {
debounce, debounce,
createLocalDate, createLocalDate,
initializeAdminCore, initializeAdminCore,
loadDashboardDataFromDashboardModule,
adminAppState adminAppState
}; };

View File

@ -20,6 +20,9 @@ class AdminCutsManager {
this.previewLayer = null; this.previewLayer = null;
this.editingCutId = null; this.editingCutId = null;
// Location markers for map display
this.locationMarkers = null;
// Bind event handler once to avoid issues with removing listeners // Bind event handler once to avoid issues with removing listeners
this.boundHandleCutActionClick = this.handleCutActionClick.bind(this); this.boundHandleCutActionClick = this.handleCutActionClick.bind(this);
} }
@ -402,6 +405,12 @@ class AdminCutsManager {
if (printBtn) { if (printBtn) {
printBtn.addEventListener('click', () => this.printUtils.printCutView()); printBtn.addEventListener('click', () => this.printUtils.printCutView());
} }
// Set up show locations toggle
const showLocationsToggle = document.getElementById('show-locations-on-map');
if (showLocationsToggle) {
showLocationsToggle.addEventListener('change', (e) => this.toggleLocationsOnMap(e.target.checked));
}
} }
// Set up toolbar controls for real-time drawing feedback // Set up toolbar controls for real-time drawing feedback
@ -1394,6 +1403,13 @@ class AdminCutsManager {
// Clear current cut // Clear current cut
this.currentCutId = null; this.currentCutId = null;
// Reset location toggle to unchecked and hide locations
const showLocationsToggle = document.getElementById('show-locations-on-map');
if (showLocationsToggle) {
showLocationsToggle.checked = false;
this.toggleLocationsOnMap(false);
}
// Clear any preview // Clear any preview
if (this.cutDrawing) { if (this.cutDrawing) {
this.cutDrawing.clearPreview(); this.cutDrawing.clearPreview();
@ -1454,6 +1470,150 @@ class AdminCutsManager {
this.showStatus('Cuts exported successfully', 'success'); this.showStatus('Cuts exported successfully', 'success');
} }
// Toggle locations visibility on the cuts map
async toggleLocationsOnMap(show) {
if (!this.cutsMap) {
console.warn('Cuts map not initialized');
return;
}
try {
if (show) {
// Load and display locations on the map
const response = await fetch('/api/locations');
const data = await response.json();
if (data.success && data.locations) {
// Remove existing location markers if any
if (this.locationMarkers) {
this.locationMarkers.clearLayers();
} else {
this.locationMarkers = L.layerGroup().addTo(this.cutsMap);
}
// Add location markers to the map
data.locations.forEach(location => {
if (location.latitude && location.longitude) {
// Handle multiple possible field names for support level
const supportLevel = location.support_level ||
location['Support Level'] ||
location.supportLevel ||
location['support level'] ||
location.Support_Level ||
'unknown';
// Debug logging for first few locations
if (data.locations.indexOf(location) < 3) {
console.log('Location debug:', {
support_level: location.support_level,
'Support Level': location['Support Level'],
supportLevel: location.supportLevel,
'support level': location['support level'],
Support_Level: location.Support_Level,
finalSupportLevel: supportLevel,
allKeys: Object.keys(location)
});
}
const marker = L.circleMarker([location.latitude, location.longitude], {
radius: 8,
fillColor: this.getLocationColor(supportLevel),
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
});
// Add popup with location info
const firstName = location.first_name || location['First Name'] || '';
const lastName = location.last_name || location['Last Name'] || '';
const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown';
const address = location.address || location.Address || '';
const email = location.email || location.Email || '';
const phone = location.phone || location.Phone || '';
const contact = [email, phone].filter(Boolean).join(', ');
const hasSign = location.sign || location.Sign ? 'Yes' : 'No';
const signSize = location.sign_size || location['Sign Size'] || '';
const notes = location.notes || location.Notes || '';
const supportLevelText = this.getSupportLevelText(supportLevel);
const popupContent = `
<div class="location-popup">
<div class="location-header">
<strong>${name}</strong>
<span class="support-level support-level-${supportLevel || 'unknown'}">${supportLevelText}</span>
</div>
<div class="location-address">
📍 ${address || 'No address available'}
</div>
${contact ? `<div class="location-contact">
📞 ${contact}
</div>` : ''}
<div class="location-details">
<div class="detail-row">
<span class="detail-label">Lawn Sign:</span>
<span class="detail-value">${hasSign}${signSize ? ` (${signSize})` : ''}</span>
</div>
${location.latitude && location.longitude ? `
<div class="detail-row">
<span class="detail-label">Coordinates:</span>
<span class="detail-value coordinates">${parseFloat(location.latitude).toFixed(6)}, ${parseFloat(location.longitude).toFixed(6)}</span>
</div>` : ''}
</div>
${notes ? `<div class="location-notes">
<strong>Notes:</strong> ${notes.length > 100 ? notes.substring(0, 100) + '...' : notes}
</div>` : ''}
</div>
`;
marker.bindPopup(popupContent, {
maxWidth: 300,
className: 'location-popup-container'
});
this.locationMarkers.addLayer(marker);
}
});
console.log(`Added ${data.locations.length} location markers to cuts map`);
} else {
console.warn('No locations data received');
}
} else {
// Hide locations
if (this.locationMarkers) {
this.cutsMap.removeLayer(this.locationMarkers);
this.locationMarkers = null;
}
console.log('Removed location markers from cuts map');
}
} catch (error) {
console.error('Error toggling locations on map:', error);
this.showStatus('Failed to load locations', 'error');
}
}
// Helper method to get color based on support level
getLocationColor(supportLevel) {
switch (String(supportLevel)) {
case '1': return '#28a745'; // Green - Strong Support
case '2': return '#ffc107'; // Yellow - Lean Support
case '3': return '#fd7e14'; // Orange - Lean Opposition
case '4': return '#dc3545'; // Red - Strong Opposition
default: return '#6c757d'; // Gray - Unknown
}
}
// Helper method to get support level text
getSupportLevelText(supportLevel) {
switch (String(supportLevel)) {
case '1': return 'Strong Support';
case '2': return 'Lean Support';
case '3': return 'Lean Opposition';
case '4': return 'Strong Opposition';
default: return 'Unknown';
}
}
async handleImportFile(event) { async handleImportFile(event) {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;

View File

@ -9,6 +9,28 @@ let startMarker = null;
// Initialize the admin map // Initialize the admin map
function initializeAdminMap() { function initializeAdminMap() {
// Check if the map container is visible before initializing
const mapContainer = document.getElementById('admin-map');
if (!mapContainer) {
console.warn('Admin map container not found, delaying initialization');
return;
}
// If container is hidden, delay initialization
const isVisible = mapContainer.offsetWidth > 0 && mapContainer.offsetHeight > 0;
if (!isVisible) {
console.log('Admin map container is hidden, delaying initialization until section is shown');
return;
}
// Avoid double initialization
if (adminMap) {
console.log('Admin map already initialized, invalidating size instead');
adminMap.invalidateSize();
return;
}
console.log('Initializing admin map...');
adminMap = L.map('admin-map').setView([53.5461, -113.4938], 11); adminMap = L.map('admin-map').setView([53.5461, -113.4938], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
@ -40,6 +62,13 @@ function initializeAdminMap() {
// Update coordinates when map moves // Update coordinates when map moves
adminMap.on('moveend', updateCoordinatesFromMap); adminMap.on('moveend', updateCoordinatesFromMap);
// Trigger size invalidation after a brief moment to ensure proper rendering
setTimeout(() => {
if (adminMap) {
adminMap.invalidateSize();
}
}, 100);
} }
// Load current start location // Load current start location

View File

@ -102,30 +102,31 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.adminCore && typeof window.adminCore.showSection === 'function') { if (window.adminCore && typeof window.adminCore.showSection === 'function') {
window.adminCore.showSection('dashboard'); window.adminCore.showSection('dashboard');
} }
// Load dashboard data on initial page load // Dashboard loading is handled by admin-core.js showSection/loadSectionData
if (typeof loadDashboardData === 'function') { // No need to manually load here to avoid duplicates
loadDashboardData();
}
} }
}); });
/** /**
* Dashboard Functions * Legacy Dashboard Functions - DEPRECATED
* Loads dashboard data and displays summary statistics * These are kept for backward compatibility but should not be used
* Use the dashboard.js module functions instead
*/ */
async function loadDashboardData() { async function loadAdminDashboardData() {
try { try {
const response = await fetch('/api/admin/dashboard'); const response = await fetch('/api/admin/dashboard');
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
// This was the old function that only loaded user/shift stats
// Now deprecated in favor of the full dashboard.js implementation
document.getElementById('total-users').textContent = data.stats.totalUsers; document.getElementById('total-users').textContent = data.stats.totalUsers;
document.getElementById('total-shifts').textContent = data.stats.totalShifts; document.getElementById('total-shifts').textContent = data.stats.totalShifts;
document.getElementById('total-signups').textContent = data.stats.totalSignups; document.getElementById('total-signups').textContent = data.stats.totalSignups;
document.getElementById('this-month-users').textContent = data.stats.thisMonthUsers; document.getElementById('this-month-users').textContent = data.stats.thisMonthUsers;
} }
} catch (error) { } catch (error) {
console.error('Failed to load dashboard data:', error); console.error('Failed to load admin dashboard data:', error);
} }
} }
@ -133,11 +134,11 @@ async function loadDashboardData() {
* Legacy function redirects for backward compatibility * Legacy function redirects for backward compatibility
* These ensure existing functionality continues to work * These ensure existing functionality continues to work
*/ */
window.loadDashboardData = loadDashboardData; window.loadAdminDashboardData = loadAdminDashboardData;
// Export dashboard function for module coordination // Export dashboard function for module coordination
if (typeof window.adminDashboard === 'undefined') { if (typeof window.adminDashboard === 'undefined') {
window.adminDashboard = { window.adminDashboard = {
loadDashboardData: loadDashboardData loadAdminDashboardData: loadAdminDashboardData
}; };
} }

View File

@ -2,9 +2,19 @@
let supportChart = null; let supportChart = null;
let entriesChart = null; let entriesChart = null;
let signSizesChart = null; let signSizesChart = null;
let dashboardLoading = false;
// Load dashboard data // Load dashboard data
async function loadDashboardData() { async function loadDashboardData() {
// Prevent multiple simultaneous loads
if (dashboardLoading) {
console.log('Dashboard already loading, skipping duplicate request');
return;
}
dashboardLoading = true;
console.log('Loading dashboard data from dashboard.js');
try { try {
// Show loading state // Show loading state
setLoadingState(true); setLoadingState(true);
@ -17,6 +27,7 @@ async function loadDashboardData() {
createSupportLevelChart(result.data.supportLevels); createSupportLevelChart(result.data.supportLevels);
createSignSizesChart(result.data.signSizes); createSignSizesChart(result.data.signSizes);
createEntriesChart(result.data.dailyEntries); createEntriesChart(result.data.dailyEntries);
console.log('Dashboard data loaded successfully from dashboard.js');
} else { } else {
showStatus('Failed to load dashboard data', 'error'); showStatus('Failed to load dashboard data', 'error');
} }
@ -25,6 +36,7 @@ async function loadDashboardData() {
showStatus('Error loading dashboard', 'error'); showStatus('Error loading dashboard', 'error');
} finally { } finally {
setLoadingState(false); setLoadingState(false);
dashboardLoading = false;
} }
} }
@ -307,32 +319,37 @@ function createEntriesChart(dailyEntries) {
// Add event listener for dashboard navigation // Add event listener for dashboard navigation
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Update navigation to load dashboard when clicked // Dashboard navigation is handled by admin-core.js
// This listener is kept for backward compatibility but should not interfere
const dashboardLink = document.querySelector('.admin-nav a[href="#dashboard"]'); const dashboardLink = document.querySelector('.admin-nav a[href="#dashboard"]');
if (dashboardLink) { if (dashboardLink) {
dashboardLink.addEventListener('click', (e) => { // Only add our listener if admin-core.js hasn't already handled it
e.preventDefault(); const hasAdminCore = window.adminCore && typeof window.adminCore.showSection === 'function';
if (!hasAdminCore) {
// Hide all sections dashboardLink.addEventListener('click', (e) => {
document.querySelectorAll('.admin-section').forEach(section => { e.preventDefault();
section.style.display = 'none';
// Hide all sections
document.querySelectorAll('.admin-section').forEach(section => {
section.style.display = 'none';
});
// Show dashboard
const dashboardSection = document.getElementById('dashboard');
if (dashboardSection) {
dashboardSection.style.display = 'block';
}
// Update active nav
document.querySelectorAll('.admin-nav a').forEach(link => {
link.classList.remove('active');
});
dashboardLink.classList.add('active');
// Load dashboard data
loadDashboardData();
}); });
}
// Show dashboard
const dashboardSection = document.getElementById('dashboard');
if (dashboardSection) {
dashboardSection.style.display = 'block';
}
// Update active nav
document.querySelectorAll('.admin-nav a').forEach(link => {
link.classList.remove('active');
});
dashboardLink.classList.add('active');
// Load dashboard data
loadDashboardData();
});
} }
// Handle window resize for chart responsiveness // Handle window resize for chart responsiveness

View File

@ -94,6 +94,10 @@ Controller for CRUD operations on map cuts (geographic polygon overlays). Handle
Controller for managing Listmonk email list synchronization. Handles sync status checking, bulk synchronization operations for locations and users, list statistics retrieval, connection testing, and list reinitialization. Provides admin-only endpoints for managing the email marketing integration. Controller for managing Listmonk email list synchronization. Handles sync status checking, bulk synchronization operations for locations and users, list statistics retrieval, connection testing, and list reinitialization. Provides admin-only endpoints for managing the email marketing integration.
# app/controllers/externalDataController.js
Controller for integrating with external data sources like the City of Edmonton's Socrata API. Handles fetching parcel addresses and other government datasets with filtering, pagination, and bounds-based queries. Provides endpoints for loading external address data to supplement the map display.
# app/middleware/auth.js # app/middleware/auth.js
Express middleware for authentication and admin access control. Express middleware for authentication and admin access control.
@ -142,6 +146,10 @@ Service for generating QR codes and handling QR-related logic.
Service for integrating with Socrata API to access external government datasets. Handles API communication with Edmonton's open data platform for retrieving parcel and address information. Service for integrating with Socrata API to access external government datasets. Handles API communication with Edmonton's open data platform for retrieving parcel and address information.
# app/services/accountExpiration.js
Service for managing temporary user account expiration. Automatically checks for and removes expired temporary user accounts on a scheduled basis. Provides cleanup functionality to maintain database hygiene and ensure temporary accounts don't persist beyond their intended lifespan.
# app/templates/email/password-recovery.txt # app/templates/email/password-recovery.txt
Plain text email template for password recovery notifications. Contains user-friendly formatting with password display and security warnings. Plain text email template for password recovery notifications. Contains user-friendly formatting with password display and security warnings.
@ -198,6 +206,14 @@ Utility functions for geographic data, validation, and helpers used across the b
Winston logger configuration for backend logging. Winston logger configuration for backend logging.
# app/utils/cacheBusting.js
Utility for managing cache busting functionality to ensure users get the latest version of the application when updates are deployed. Handles versioning and cache invalidation strategies with server-side version generation and file hash management.
# app/utils/spatial.js
Utility for spatial operations including point-in-polygon calculations and geographic boundary operations. Provides functions for determining if locations fall within cut boundaries and other spatial analysis needed for map-based filtering and location management.
# app/public/admin.html # app/public/admin.html
Admin panel HTML page for managing start location, walk sheet, shift management, user management, and email broadcasting. Features rich text editor with live preview for composing broadcast emails, shift volunteer management modals, comprehensive admin interface with user role controls, and quick access links to both NocoDB database management and Listmonk email marketing interfaces. Admin panel HTML page for managing start location, walk sheet, shift management, user management, and email broadcasting. Features rich text editor with live preview for composing broadcast emails, shift volunteer management modals, comprehensive admin interface with user role controls, and quick access links to both NocoDB database management and Listmonk email marketing interfaces.
@ -382,6 +398,10 @@ User profile page HTML for displaying user information and account management.
CSS styles for the user profile page and user management components in the admin panel. CSS styles for the user profile page and user management components in the admin panel.
# app/public/css/REFACTORING_SUMMARY.md
Documentation summarizing the CSS refactoring process that reorganized the admin panel styles into modular components. Describes the transition from monolithic CSS files to focused modules for better maintainability and organization.
# app/public/js/admin.js # app/public/js/admin.js
**Main Admin Panel Coordinator** - Coordinates all admin modules and handles initialization. Contains the primary DOM ready handler that initializes all admin functionality, manages section routing, and provides coordination between all modular components. Maintains backward compatibility and global function exports. **Main Admin Panel Coordinator** - Coordinates all admin modules and handles initialization. Contains the primary DOM ready handler that initializes all admin functionality, manages section routing, and provides coordination between all modular components. Maintains backward compatibility and global function exports.
@ -490,6 +510,18 @@ Frontend JavaScript for the Convert Data admin section. Handles file upload UI,
JavaScript module for interactive polygon drawing functionality. Implements click-to-add-points drawing system for creating cut boundaries on the map using Leaflet.js drawing tools. JavaScript module for interactive polygon drawing functionality. Implements click-to-add-points drawing system for creating cut boundaries on the map using Leaflet.js drawing tools.
# app/public/js/cut-drawing-new.js
**New Cut Drawing Module** - Enhanced version of the polygon drawing functionality for cut creation. Provides improved polygon drawing with vertex management, polygon preview, color customization, and enhanced user interaction. Implements click-to-add-points drawing system with better visual feedback and drawing state management.
# app/public/js/cut-location-manager.js
**Cut Location Management Module** - Handles location display, filtering, statistics, and management specifically for cuts. Manages location markers within cut boundaries, provides filtering capabilities, calculates cut statistics, and handles the display of locations associated with specific cuts on the map.
# app/public/js/cut-print-utils.js
**Cut Print Utilities Module** - Handles map capture, print generation, and export functionality specifically for cuts. Provides functionality to generate printable views of cuts with associated locations, handles map capture for printing, and manages export formats for cut data and visualizations.
# app/public/js/cut-controls.js # app/public/js/cut-controls.js
JavaScript module for cut display controls on the public map. Handles loading and rendering of public cuts as polygon overlays for authenticated users. JavaScript module for cut display controls on the public map. Handles loading and rendering of public cuts as polygon overlays for authenticated users.
@ -498,6 +530,22 @@ JavaScript module for cut display controls on the public map. Handles loading an
JavaScript for the admin cut management interface. Provides complete CRUD functionality for cuts including interactive drawing, form management, cut list display, and import/export capabilities. JavaScript for the admin cut management interface. Provides complete CRUD functionality for cuts including interactive drawing, form management, cut list display, and import/export capabilities.
# app/public/js/admin-cuts-main.js
**Admin Cuts Main Coordinator** - Main initialization file that imports and orchestrates all cut management modules. Handles DOM ready initialization, module loading verification, and coordinates the startup sequence for the admin cuts management system.
# app/public/js/admin-cuts-manager.js
**Admin Cuts Manager Module** - Main class for managing cuts with comprehensive form handling, UI interactions, and cut operations. Handles cut creation, editing, deletion, filtering, drawing operations, location management, and provides the core administrative interface for cut management with integrated map functionality.
# app/public/js/admin-new.js
**New Admin Panel Coordinator** - Refactored version of the admin panel coordinator that coordinates all admin modules while maintaining functionality. Manages event listener setup across modules including auth, map, walksheet, shifts, users, email, and integration modules with improved modular architecture.
# app/public/js/admin-old.js
**Legacy Admin Panel Coordinator** - Backup or legacy version of the admin panel coordinator, likely containing the previous monolithic implementation before modular refactoring. Maintains the same functionality as the newer version but with older architecture for reference or rollback purposes.
# app/public/js/listmonk-status.js # app/public/js/listmonk-status.js
JavaScript module for real-time Listmonk sync status monitoring. Displays connection status indicators in the UI, shows error notifications when sync fails, handles automatic status checking, and provides user feedback for email list synchronization health. JavaScript module for real-time Listmonk sync status monitoring. Displays connection status indicators in the UI, shows error notifications when sync fails, handles automatic status checking, and provides user feedback for email list synchronization health.
@ -598,6 +646,10 @@ Express router for public-facing endpoints that don't require authentication. Ha
Express router for user management endpoints (list, create, delete users). Express router for user management endpoints (list, create, delete users).
# app/routes/publicShifts.js
Express router for public-facing shift endpoints that don't require authentication. Handles public shift viewing, shift details retrieval, and public signup functionality with appropriate rate limiting. Provides the API endpoints for the public volunteer opportunity signup system.
# app/routes/cuts.js # app/routes/cuts.js
Express router for cut management endpoints. Provides CRUD operations for geographic polygon overlays with admin-only access for modifications and public read access for viewing public cuts. Express router for cut management endpoints. Provides CRUD operations for geographic polygon overlays with admin-only access for modifications and public read access for viewing public cuts.
@ -614,6 +666,18 @@ Express router for external data integration endpoints, including Socrata API in
Utility for managing cache busting functionality to ensure users get the latest version of the application when updates are deployed. Handles versioning and cache invalidation strategies. Utility for managing cache busting functionality to ensure users get the latest version of the application when updates are deployed. Handles versioning and cache invalidation strategies.
# package.json
Node.js project manifest for the main map application workspace. Contains project dependencies, scripts, and configuration for the overall map application development and deployment process.
# package-lock.json
Automatically generated lockfile that ensures consistent dependency installation across environments for the main map application workspace.
# test_print_debug.html
Debug HTML file for testing print functionality and troubleshooting print-related features. Contains test layouts and styling for validating print output of maps, walk sheets, and other printable components.
# listmonk-env-example.txt # listmonk-env-example.txt
Example environment configuration file showing the required Listmonk environment variables. Provides sample configuration for API URL, credentials, sync settings, and setup instructions for integrating with the Listmonk email marketing platform. Example environment configuration file showing the required Listmonk environment variables. Provides sample configuration for API URL, credentials, sync settings, and setup instructions for integrating with the Listmonk email marketing platform.