Cut updates. Still need to test the assignment system.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user