Cut updates. Still need to test the assignment system.
This commit is contained in:
parent
459cea0c3b
commit
bc08f0b55d
@ -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>
|
||||
|
||||
<div class="cuts-container">
|
||||
<!-- Map and Drawing Controls -->
|
||||
<div class="cuts-map-section">
|
||||
<div id="cuts-map" class="admin-map"></div>
|
||||
|
||||
<!-- Drawing Toolbar -->
|
||||
<div id="cut-drawing-toolbar" class="cut-drawing-toolbar">
|
||||
<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>
|
||||
<!-- Top Section with Existing Cuts and Form -->
|
||||
<div class="cuts-top-section">
|
||||
<!-- Existing Cuts List - Moved to Top -->
|
||||
<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">
|
||||
<button id="refresh-cuts-btn" class="btn btn-secondary btn-sm">Refresh</button>
|
||||
<button id="export-cuts-btn" class="btn btn-secondary btn-sm">Export All</button>
|
||||
<label for="import-cuts-file" class="btn btn-secondary btn-sm" style="margin: 0;">
|
||||
Import
|
||||
<input type="file" id="import-cuts-file" accept=".json" style="display: none;">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<div class="panel-content">
|
||||
<div class="cuts-filters">
|
||||
<input type="text" id="cuts-search" placeholder="Search cuts..." class="form-control">
|
||||
<select id="cuts-category-filter" class="form-control">
|
||||
<option value="">All Categories</option>
|
||||
<option value="Custom">Custom</option>
|
||||
<option value="Ward">Ward</option>
|
||||
<option value="Neighborhood">Neighborhood</option>
|
||||
<option value="District">District</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="cut-public" name="is_public" checked>
|
||||
<label for="cut-public">Make this cut visible on the public map</label>
|
||||
<div id="cuts-list" class="cuts-list">
|
||||
<!-- Cuts will be populated here -->
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="cut-official" name="is_official">
|
||||
<label for="cut-official">Mark as official cut</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 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 class="panel-content">
|
||||
<form id="cut-form" class="cut-form cut-form-compact">
|
||||
<!-- 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-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>
|
||||
|
||||
<!-- Cut Location Management -->
|
||||
<!-- Cut Location Management - Moved Above Map -->
|
||||
<div class="cuts-location-section" id="cut-location-management" style="display: none;">
|
||||
<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">
|
||||
<!-- Location Filters -->
|
||||
<div class="location-filters">
|
||||
@ -971,36 +976,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cuts List -->
|
||||
<div class="cuts-list-section">
|
||||
<div class="cuts-management-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">Existing Cuts</h3>
|
||||
|
||||
<!-- Location Management Footer Actions -->
|
||||
<div class="panel-footer">
|
||||
<h3 class="panel-title">Location Management</h3>
|
||||
<div class="panel-actions">
|
||||
<button id="refresh-cuts-btn" class="btn btn-secondary btn-sm">Refresh</button>
|
||||
<button id="export-cuts-btn" class="btn btn-secondary btn-sm">Export All</button>
|
||||
<label for="import-cuts-file" class="btn btn-secondary btn-sm" style="margin: 0;">
|
||||
Import
|
||||
<input type="file" id="import-cuts-file" accept=".json" style="display: none;">
|
||||
</label>
|
||||
<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="cuts-filters">
|
||||
<input type="text" id="cuts-search" placeholder="Search cuts..." class="form-control">
|
||||
<select id="cuts-category-filter" class="form-control">
|
||||
<option value="">All Categories</option>
|
||||
<option value="Custom">Custom</option>
|
||||
<option value="Ward">Ward</option>
|
||||
<option value="Neighborhood">Neighborhood</option>
|
||||
<option value="District">District</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map and Drawing Controls -->
|
||||
<div class="cuts-map-section">
|
||||
<div id="cuts-map" class="admin-map"></div>
|
||||
|
||||
<!-- Drawing Toolbar -->
|
||||
<div id="cut-drawing-toolbar" class="cut-drawing-toolbar">
|
||||
<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 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>
|
||||
|
||||
@ -8,6 +8,63 @@
|
||||
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 {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
@ -24,8 +81,9 @@
|
||||
}
|
||||
|
||||
.cuts-form-section,
|
||||
.cuts-list-section {
|
||||
margin-top: var(--padding-base);
|
||||
.cuts-list-section,
|
||||
.cuts-location-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.cuts-filters {
|
||||
@ -42,6 +100,122 @@
|
||||
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 */
|
||||
.cut-form.disabled {
|
||||
opacity: 0.7;
|
||||
@ -196,23 +370,37 @@
|
||||
|
||||
/* Large Screen Layout for Cuts */
|
||||
@media (min-width: 1200px) {
|
||||
.cuts-container {
|
||||
.cuts-top-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: var(--padding-base);
|
||||
}
|
||||
|
||||
.cuts-form-section,
|
||||
.cuts-list-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--padding-base);
|
||||
margin-top: 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.cuts-form-section > *,
|
||||
.cuts-list-section > * {
|
||||
grid-column: span 1;
|
||||
.cuts-form-section {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@ -1185,7 +1185,7 @@
|
||||
}
|
||||
|
||||
.cuts-location-section .panel-title {
|
||||
color: white;
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.cuts-location-section .btn-sm {
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
|
||||
// Global admin state
|
||||
let adminAppState = {
|
||||
currentSection: 'dashboard'
|
||||
currentSection: 'dashboard',
|
||||
dashboardLoading: false
|
||||
};
|
||||
|
||||
// Utility function to create a local date from YYYY-MM-DD string
|
||||
@ -173,9 +174,8 @@ function loadSectionData(sectionId) {
|
||||
}
|
||||
break;
|
||||
case 'dashboard':
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
}
|
||||
// Always use our guarded dashboard loading function
|
||||
loadDashboardDataFromDashboardModule();
|
||||
break;
|
||||
case 'shifts':
|
||||
if (typeof loadAdminShifts === 'function') {
|
||||
@ -188,6 +188,21 @@ function loadSectionData(sectionId) {
|
||||
loadUsers();
|
||||
}
|
||||
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':
|
||||
// Initialize data convert event listeners when section is shown
|
||||
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
|
||||
function initializeAdminCore() {
|
||||
// Set initial viewport dimensions and listen for resize events
|
||||
@ -324,5 +404,6 @@ window.adminCore = {
|
||||
debounce,
|
||||
createLocalDate,
|
||||
initializeAdminCore,
|
||||
loadDashboardDataFromDashboardModule,
|
||||
adminAppState
|
||||
};
|
||||
|
||||
@ -20,6 +20,9 @@ class AdminCutsManager {
|
||||
this.previewLayer = null;
|
||||
this.editingCutId = null;
|
||||
|
||||
// Location markers for map display
|
||||
this.locationMarkers = null;
|
||||
|
||||
// Bind event handler once to avoid issues with removing listeners
|
||||
this.boundHandleCutActionClick = this.handleCutActionClick.bind(this);
|
||||
}
|
||||
@ -402,6 +405,12 @@ class AdminCutsManager {
|
||||
if (printBtn) {
|
||||
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
|
||||
@ -1394,6 +1403,13 @@ class AdminCutsManager {
|
||||
// Clear current cut
|
||||
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
|
||||
if (this.cutDrawing) {
|
||||
this.cutDrawing.clearPreview();
|
||||
@ -1454,6 +1470,150 @@ class AdminCutsManager {
|
||||
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) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
@ -9,6 +9,28 @@ let startMarker = null;
|
||||
|
||||
// Initialize the admin map
|
||||
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);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
@ -40,6 +62,13 @@ function initializeAdminMap() {
|
||||
|
||||
// Update coordinates when map moves
|
||||
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
|
||||
|
||||
@ -102,30 +102,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.adminCore && typeof window.adminCore.showSection === 'function') {
|
||||
window.adminCore.showSection('dashboard');
|
||||
}
|
||||
// Load dashboard data on initial page load
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
}
|
||||
// Dashboard loading is handled by admin-core.js showSection/loadSectionData
|
||||
// No need to manually load here to avoid duplicates
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Dashboard Functions
|
||||
* Loads dashboard data and displays summary statistics
|
||||
* Legacy Dashboard Functions - DEPRECATED
|
||||
* 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 {
|
||||
const response = await fetch('/api/admin/dashboard');
|
||||
const data = await response.json();
|
||||
|
||||
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-shifts').textContent = data.stats.totalShifts;
|
||||
document.getElementById('total-signups').textContent = data.stats.totalSignups;
|
||||
document.getElementById('this-month-users').textContent = data.stats.thisMonthUsers;
|
||||
}
|
||||
} 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
|
||||
* These ensure existing functionality continues to work
|
||||
*/
|
||||
window.loadDashboardData = loadDashboardData;
|
||||
window.loadAdminDashboardData = loadAdminDashboardData;
|
||||
|
||||
// Export dashboard function for module coordination
|
||||
if (typeof window.adminDashboard === 'undefined') {
|
||||
window.adminDashboard = {
|
||||
loadDashboardData: loadDashboardData
|
||||
loadAdminDashboardData: loadAdminDashboardData
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,9 +2,19 @@
|
||||
let supportChart = null;
|
||||
let entriesChart = null;
|
||||
let signSizesChart = null;
|
||||
let dashboardLoading = false;
|
||||
|
||||
// Load dashboard data
|
||||
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 {
|
||||
// Show loading state
|
||||
setLoadingState(true);
|
||||
@ -17,6 +27,7 @@ async function loadDashboardData() {
|
||||
createSupportLevelChart(result.data.supportLevels);
|
||||
createSignSizesChart(result.data.signSizes);
|
||||
createEntriesChart(result.data.dailyEntries);
|
||||
console.log('Dashboard data loaded successfully from dashboard.js');
|
||||
} else {
|
||||
showStatus('Failed to load dashboard data', 'error');
|
||||
}
|
||||
@ -25,6 +36,7 @@ async function loadDashboardData() {
|
||||
showStatus('Error loading dashboard', 'error');
|
||||
} finally {
|
||||
setLoadingState(false);
|
||||
dashboardLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,32 +319,37 @@ function createEntriesChart(dailyEntries) {
|
||||
|
||||
// Add event listener for dashboard navigation
|
||||
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"]');
|
||||
if (dashboardLink) {
|
||||
dashboardLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide all sections
|
||||
document.querySelectorAll('.admin-section').forEach(section => {
|
||||
section.style.display = 'none';
|
||||
// Only add our listener if admin-core.js hasn't already handled it
|
||||
const hasAdminCore = window.adminCore && typeof window.adminCore.showSection === 'function';
|
||||
if (!hasAdminCore) {
|
||||
dashboardLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 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
|
||||
|
||||
@ -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.
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
||||
**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.
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
||||
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).
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user