diff --git a/map/README.md b/map/README.md index 522e86e..2f01973 100644 --- a/map/README.md +++ b/map/README.md @@ -15,10 +15,12 @@ A containerized web application that visualizes geographic data from NocoDB on a - 🎯 Configurable map start location - 📋 Walk Sheet generator for door-to-door canvassing - 🔗 QR code integration for digital resources -- � Volunteer shift management system with calendar and grid views +- 📅 Volunteer shift management system with calendar and grid views - ✋ User shift signup and cancellation with color-coded calendar - 👥 Admin shift creation and management -- �🐳 Docker containerization for easy deployment +- 👨‍💼 User management panel for admin users (create, delete users) +- 🔐 Role-based access control (Admin vs User permissions) +- 🐳 Docker containerization for easy deployment - 🆓 100% open source (no proprietary dependencies) ## Quick Start @@ -140,7 +142,7 @@ The build script automatically creates the following table structure: - `Support Level` (Single Select): Options: "1", "2", "3", "4" (1=Strong Support/Green, 2=Moderate Support/Yellow, 3=Low Support/Orange, 4=No Support/Red) - `Address` (Single Line Text): Street address - `Sign` (Checkbox): Has campaign sign -- `Sign Size` (Single Select): Options: "Small", "Medium", "Large" +- `Sign Size` (Single Select): Options: "Regular", "Large", "Unsure" - `Notes` (Long Text): Additional details and comments - `title` (Text): Location name (legacy field) - `category` (Single Select): Classification (legacy field) @@ -272,14 +274,27 @@ Users with admin privileges can access the admin panel at `/admin.html` to confi - **Live Preview**: See changes as you type - **Print Optimization**: Proper formatting for printing or PDF export - **Persistent Storage**: All QR codes and settings saved to NocoDB -- **Real-time Preview**: See changes immediately on the admin map -- **Validation**: Built-in coordinate and zoom level validation + +#### Shift Management + +- **Create Shifts**: Set up volunteer shifts with dates, times, and capacity +- **Manage Volunteers**: View signups and manage shift participants +- **Real-time Updates**: See shift status changes immediately + +#### User Management + +- **Create Users**: Add new user accounts with email and password +- **Role Assignment**: Assign admin or user privileges +- **User List**: View all registered users with their details and creation dates +- **Delete Users**: Remove user accounts (with confirmation prompts) +- **Security**: Password validation and admin-only access ### Access Control - Admin access is controlled via the `Admin` checkbox in the Login table - Only authenticated users with admin privileges can access `/admin.html` - Admin status is checked on every request to admin endpoints +- User management functions are restricted to admin users only ### Start Location Priority diff --git a/map/app/controllers/usersController.js b/map/app/controllers/usersController.js index 045a57b..c265c8a 100644 --- a/map/app/controllers/usersController.js +++ b/map/app/controllers/usersController.js @@ -6,19 +6,34 @@ const { sanitizeUser, extractId } = require('../utils/helpers'); class UsersController { async getAll(req, res) { try { + // Debug logging + logger.info('UsersController.getAll called'); + logger.info('loginSheetId from config:', config.nocodb.loginSheetId); + logger.info('NocoDB config:', { + apiUrl: config.nocodb.apiUrl, + hasToken: !!config.nocodb.apiToken, + projectId: config.nocodb.projectId, + tableId: config.nocodb.tableId, + loginSheetId: config.nocodb.loginSheetId + }); + if (!config.nocodb.loginSheetId) { + logger.error('Login sheet not configured in environment'); return res.status(500).json({ success: false, - error: 'Login sheet not configured' + error: 'Login sheet not configured. Please set NOCODB_LOGIN_SHEET in your environment variables.' }); } + logger.info('Fetching users from NocoDB...'); + // Remove the sort parameter that's causing the error const response = await nocodbService.getAll(config.nocodb.loginSheetId, { - limit: 100, - sort: '-created_at' + limit: 100 + // Removed: sort: '-created_at' }); const users = response.list || []; + logger.info(`Retrieved ${users.length} users from database`); // Remove password field from response for security const safeUsers = users.map(sanitizeUser); @@ -32,7 +47,7 @@ class UsersController { logger.error('Error fetching users:', error); res.status(500).json({ success: false, - error: 'Failed to fetch users' + error: 'Failed to fetch users: ' + error.message }); } } @@ -65,7 +80,7 @@ class UsersController { }); } - // Create new user + // Create new user - use the actual column names from your table const userData = { Email: email, email: email, @@ -74,9 +89,8 @@ class UsersController { Name: name || '', name: name || '', Admin: admin === true, - admin: admin === true, - 'Created At': new Date().toISOString(), - created_at: new Date().toISOString() + admin: admin === true + // Removed created_at fields as they might not exist }; const response = await nocodbService.create( diff --git a/map/app/public/admin.html b/map/app/public/admin.html index bac2651..f1dbd3a 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -38,6 +38,7 @@ Start Location Walk Sheet Shifts + User Management @@ -239,6 +240,71 @@ + + + diff --git a/map/app/public/css/admin.css b/map/app/public/css/admin.css index 4daff93..18c487e 100644 --- a/map/app/public/css/admin.css +++ b/map/app/public/css/admin.css @@ -564,90 +564,297 @@ color: var(--warning-color); } -/* Responsive - Scale down for smaller screens */ -@media (max-width: 1400px) { - .walk-sheet-preview .walk-sheet-page { - transform: scale(0.85); - transform-origin: top center; - } - .walk-sheet-preview { - min-height: 850px; - } +/* User Management Styles */ +.users-admin-container { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 30px; + margin-top: 20px; } -/* Mobile/Small Screen Layout - Stack config above preview */ -@media (max-width: 1200px) { - .walk-sheet-container { - display: flex !important; /* Change from grid to flex */ - flex-direction: column !important; /* Stack vertically */ +.user-form, +.users-list { + background: white; + border-radius: var(--border-radius); + padding: 25px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; +} + +.user-form h3, +.users-list h3 { + margin-bottom: 20px; + color: var(--dark-color); + border-bottom: 2px solid var(--primary-color); + padding-bottom: 10px; + font-size: 18px; +} + +.users-table { + width: 100%; + border-collapse: collapse; + margin-top: 15px; + font-size: 14px; +} + +.users-table th, +.users-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #e0e0e0; +} + +.users-table th { + background-color: #f8f9fa; + font-weight: 600; + color: var(--dark-color); + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.users-table tbody tr { + transition: background-color 0.2s; +} + +.users-table tbody tr:hover { + background-color: #f8f9fa; +} + +.user-role { + padding: 4px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.user-role.admin { + background-color: #dc3545; + color: white; +} + +.user-role.user { + background-color: #28a745; + color: white; +} + +.user-actions { + display: flex; + gap: 8px; +} + +.user-actions .btn { + padding: 6px 12px; + font-size: 12px; + border-radius: var(--border-radius); + font-weight: 500; +} + +.btn-danger { + background-color: #dc3545; + color: white; + border: 1px solid #dc3545; + transition: var(--transition); +} + +.btn-danger:hover { + background-color: #c82333; + border-color: #c82333; + transform: translateY(-1px); +} + +.loading-message, +.empty-message { + text-align: center; + padding: 40px 20px; + color: #666; + font-style: italic; + background-color: #f8f9fa; + border-radius: var(--border-radius); + margin-top: 15px; +} + +.loading-message { + background-color: #e3f2fd; + color: #1976d2; +} + +/* User form specific styles */ +.user-form .form-group { + margin-bottom: 20px; +} + +.user-form .form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: var(--dark-color); + font-size: 14px; +} + +.user-form .form-group label input[type="checkbox"] { + margin-right: 8px; + transform: scale(1.1); +} + +.user-form input[type="email"], +.user-form input[type="password"], +.user-form input[type="text"] { + width: 100%; + padding: 12px; + border: 2px solid #ddd; + border-radius: var(--border-radius); + font-size: 14px; + transition: var(--transition); + background-color: #fafafa; +} + +.user-form input:focus { + outline: none; + border-color: var(--primary-color); + background-color: white; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); +} + +.user-form .form-actions { + display: flex; + gap: 10px; + margin-top: 25px; +} + +.user-form .form-actions .btn { + flex: 1; + padding: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + font-size: 13px; +} + +/* Responsive design for user management */ +@media (max-width: 768px) { + .users-admin-container { + grid-template-columns: 1fr; gap: 20px; } - .walk-sheet-config { - order: 1 !important; /* Config first */ - margin-bottom: 20px; + .users-table { + font-size: 12px; } - .walk-sheet-preview { - order: 2 !important; /* Preview second */ - padding: 20px; - min-height: auto; - max-width: 100vw; - overflow-x: auto; /* Allow horizontal scroll if needed */ - display: flex; - justify-content: center; /* Center the page */ + .users-table th, + .users-table td { + padding: 8px; } - .walk-sheet-preview .walk-sheet-page { - transform: scale(0.75); - transform-origin: top center; - margin-bottom: -200px; - max-width: 100%; /* Prevent overflow */ + .user-actions { + flex-direction: column; + gap: 4px; } -} - -@media (max-width: 1000px) { - .walk-sheet-preview .walk-sheet-page { - transform: scale(0.5); - margin-bottom: -400px; + + .user-actions .btn { + font-size: 11px; + padding: 4px 8px; } -} - -@media (max-width: 768px) { + + .user-form .form-actions { + flex-direction: column; + } + + /* Admin container mobile layout */ .admin-container { flex-direction: column; } + .admin-sidebar { width: 100%; border-right: none; border-bottom: 1px solid #e0e0e0; + padding: 15px; } + + .admin-nav { + flex-direction: row; + overflow-x: auto; + gap: 10px; + padding-bottom: 10px; + } + + .admin-nav a { + white-space: nowrap; + min-width: auto; + padding: 8px 12px; + font-size: 14px; + } + .header .header-actions { display: flex !important; gap: 10px; + flex-wrap: wrap; } + .header .header-actions .btn { padding: 6px 10px; font-size: 13px; } + .admin-info { font-size: 12px; } + .admin-map-container { grid-template-columns: 1fr; + gap: 15px; } + .admin-map { - height: 220px; + height: 250px; } + .admin-content { - padding: 8px; + padding: 15px; } + .admin-section { - padding: 10px; + padding: 15px; } + .form-row { grid-template-columns: 1fr; } + + /* Shifts admin mobile */ + .shifts-admin-container { + grid-template-columns: 1fr; + gap: 20px; + } + + .shift-admin-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .shift-actions { + width: 100%; + justify-content: flex-end; + } + + /* Walk sheet container mobile */ + .walk-sheet-container { + display: flex !important; + flex-direction: column !important; + gap: 20px; + } + + .walk-sheet-config { + order: 1 !important; + margin-bottom: 20px; + } + + /* Walk sheet mobile */ .walk-sheet-preview { min-width: 0; max-width: 100%; @@ -673,20 +880,124 @@ left: 50%; margin-left: -408px; /* Half of 816px to center it */ } -} - -/* Even smaller screens */ -@media (max-width: 480px) { - .walk-sheet-preview { - padding: 5px; - width: 100%; + + /* Status container mobile */ + .status-container { + top: 10px; + right: 10px; + left: 10px; + max-width: none; + display: flex; + flex-direction: column; + align-items: center; } - .walk-sheet-preview #walk-sheet-preview-content { + .status-message { + font-size: 13px; + padding: 10px 12px; + max-width: 90%; + text-align: center; + } +} + +@media (max-width: 480px) { + .users-table { + font-size: 11px; + } + + .user-role { + font-size: 10px; + padding: 2px 6px; + } + + .user-form input[type="email"], + .user-form input[type="password"], + .user-form input[type="text"] { + padding: 10px; + font-size: 13px; + } + + /* Very small screen adjustments */ + .admin-content { + padding: 10px; + } + + .admin-section { + padding: 10px; + } + + .admin-sidebar { + padding: 10px; + } + + .admin-nav a { + padding: 6px 10px; + font-size: 13px; + } + + .header .header-actions .btn { + padding: 5px 8px; + font-size: 12px; + } + + .admin-info { + display: none; /* Hide on very small screens */ + } + + .admin-map { + height: 200px; + } + + /* Walk sheet very small screens */ + .walk-sheet-preview { + padding: 10px; + width: 100%; + display: flex; + justify-content: center; + align-items: flex-start; + } + + .walk-sheet-preview .walk-sheet-page { transform: scale(0.25); + transform-origin: center top; margin-bottom: -750px; - left: 50%; - margin-left: -408px; /* Keep centered */ + margin-left: auto; + margin-right: auto; + } + + /* Form adjustments */ + .form-group input, + .form-group select { + font-size: 16px; /* Prevent zoom on iOS */ + } + + .btn { + min-height: 44px; /* Touch-friendly button size */ + padding: 10px 16px; + } + + .btn-sm { + min-height: 36px; + padding: 8px 12px; + } + + /* User management specific mobile adjustments */ + .user-container { + padding: 0.5rem; + } + + .user-profile { + padding: 1rem; + } + + .users-table th { + font-size: 10px; + } + + .user-actions .btn { + font-size: 10px; + padding: 3px 6px; + min-height: 32px; } } @@ -813,3 +1124,150 @@ grid-template-columns: 1fr; } } + +/* Additional mobile styles for better responsiveness */ +@media (max-width: 1200px) { + .walk-sheet-container { + display: flex !important; + flex-direction: column !important; + gap: 20px; + } + + .walk-sheet-config { + order: 1 !important; + margin-bottom: 20px; + } + + .walk-sheet-preview { + order: 2 !important; + padding: 20px; + min-height: auto; + max-width: 100vw; + overflow-x: auto; + display: flex; + justify-content: center; + align-items: flex-start; + } + + .walk-sheet-preview .walk-sheet-page { + transform: scale(0.75); + transform-origin: center top; + margin-bottom: -200px; + max-width: 100%; + margin-left: auto; + margin-right: auto; + } +} + +@media (max-width: 1000px) { + .walk-sheet-preview .walk-sheet-page { + transform: scale(0.5); + transform-origin: center top; + margin-bottom: -400px; + margin-left: auto; + margin-right: auto; + } +} + +/* Tablet and mobile header adjustments */ +@media (max-width: 1024px) { + .header { + padding: 10px 15px; + } + + .header h1 { + font-size: 20px; + } + + .header .header-actions { + gap: 8px; + } +} + +/* Responsive Table Styles for Mobile */ +@media (max-width: 640px) { + .users-table { + border: 0; + } + + .users-table thead { + border: none; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } + + .users-table tr { + border-bottom: 3px solid #ddd; + display: block; + margin-bottom: 10px; + padding: 10px; + background: #f8f9fa; + border-radius: 6px; + } + + .users-table td { + border: none; + border-bottom: 1px solid #eee; + display: block; + font-size: 13px; + text-align: right; + padding: 8px 10px; + } + + .users-table td::before { + content: attr(data-label); + float: left; + font-weight: bold; + text-transform: uppercase; + font-size: 11px; + color: #666; + } + + .users-table td:last-child { + border-bottom: 0; + } + + .user-actions { + justify-content: flex-end; + margin-top: 8px; + } + + .user-actions .btn { + width: auto !important; + min-width: 80px; + flex: none; + } +} + +/* Enhanced mobile form styling */ +@media (max-width: 480px) { + .user-form input[type="email"], + .user-form input[type="password"], + .user-form input[type="text"] { + font-size: 16px; /* Prevent zoom on iOS */ + padding: 12px; + } + + .user-form .form-group label { + font-size: 14px; + } + + .user-form .form-actions .btn { + width: 100%; + margin-bottom: 8px; + } + + .users-table td { + font-size: 12px; + padding: 6px 8px; + } + + .users-table td::before { + font-size: 10px; + } +} diff --git a/map/app/public/css/user.css b/map/app/public/css/user.css new file mode 100644 index 0000000..860deac --- /dev/null +++ b/map/app/public/css/user.css @@ -0,0 +1,324 @@ +/* User Profile Styles */ + +.user-container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} + +.user-profile { + background: #ffffff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; +} + +.user-profile h2 { + margin-bottom: 1.5rem; + color: #333; + border-bottom: 2px solid #007bff; + padding-bottom: 0.5rem; +} + +.profile-info { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.profile-info .form-group { + display: flex; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid #f0f0f0; +} + +.profile-info .form-group:last-child { + border-bottom: none; +} + +.profile-info label { + font-weight: 600; + color: #555; + min-width: 100px; + margin-right: 1rem; +} + +.profile-info span { + color: #333; + flex: 1; +} + +/* Users Admin Table Styles (for admin panel) */ +.users-admin-container { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; + margin-top: 1rem; +} + +.user-form, +.users-list { + background: #ffffff; + border-radius: 8px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; +} + +.user-form h3, +.users-list h3 { + margin-bottom: 1rem; + color: #333; + border-bottom: 2px solid #007bff; + padding-bottom: 0.5rem; +} + +.users-table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +.users-table th, +.users-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid #e0e0e0; +} + +.users-table th { + background-color: #f8f9fa; + font-weight: 600; + color: #555; +} + +.users-table tbody tr:hover { + background-color: #f8f9fa; +} + +.user-role { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 600; +} + +.user-role.admin { + background-color: #dc3545; + color: white; +} + +.user-role.user { + background-color: #28a745; + color: white; +} + +.user-actions { + display: flex; + gap: 0.5rem; +} + +.user-actions .btn { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +.btn-danger { + background-color: #dc3545; + color: white; + border: 1px solid #dc3545; +} + +.btn-danger:hover { + background-color: #c82333; + border-color: #c82333; +} + +.loading-message, +.empty-message { + text-align: center; + padding: 2rem; + color: #666; + font-style: italic; +} + +/* Form styles specific to user creation */ +.user-form .form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: #333; +} + +.user-form .form-group label input[type="checkbox"] { + margin-right: 0.5rem; +} + +.user-form input[type="email"], +.user-form input[type="password"], +.user-form input[type="text"] { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.user-form input:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); +} + +/* Responsive design */ +@media (max-width: 768px) { + .users-admin-container { + grid-template-columns: 1fr; + gap: 1rem; + } + + .users-table { + font-size: 0.9rem; + } + + .users-table th, + .users-table td { + padding: 0.5rem; + } + + .user-container { + padding: 1rem; + } + + /* Header mobile adjustments */ + .header { + padding: 10px 15px; + } + + .header h1 { + font-size: 20px; + } + + .header .header-actions { + display: flex !important; + gap: 8px; + flex-wrap: wrap; + } + + .header .header-actions .btn { + padding: 6px 10px; + font-size: 13px; + } + + .user-info { + font-size: 12px; + } + + /* User profile mobile */ + .user-profile { + padding: 1.5rem; + } + + .profile-info .form-group { + padding: 0.5rem 0; + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .profile-info label { + min-width: auto; + margin-right: 0; + font-size: 14px; + } + + .profile-info span { + font-size: 14px; + } + + /* Ensure all buttons are touch-friendly */ + .btn { + min-height: 44px; + padding: 12px 16px; + font-size: 14px; + } + + .btn-sm { + min-height: 36px; + padding: 8px 12px; + font-size: 13px; + } + + /* Better spacing for form elements */ + .form-group { + margin-bottom: 20px; + } + + /* Improve input focus visibility */ + input:focus, + select:focus, + textarea:focus { + outline: 2px solid #007bff; + outline-offset: 2px; + } +} + +@media (max-width: 480px) { + .users-table { + font-size: 0.8rem; + } + + .user-actions { + flex-direction: column; + gap: 0.25rem; + } + + .user-actions .btn { + font-size: 0.7rem; + } + + .user-container { + padding: 0.5rem; + } + + /* Very small screen adjustments */ + .header .header-actions .btn { + padding: 5px 8px; + font-size: 12px; + min-height: 36px; + } + + .user-info { + display: none; /* Hide on very small screens to save space */ + } + + .user-profile { + padding: 1rem; + } + + .user-profile h2 { + font-size: 18px; + margin-bottom: 1rem; + } + + .profile-info .form-group { + padding: 0.4rem 0; + } + + .profile-info label { + font-size: 13px; + font-weight: 500; + } + + .profile-info span { + font-size: 13px; + } + + /* Touch-friendly logout button */ + #logout-btn { + min-height: 44px; + padding: 10px 16px; + } +} diff --git a/map/app/public/index.html b/map/app/public/index.html index 4785ada..11d8c92 100644 --- a/map/app/public/index.html +++ b/map/app/public/index.html @@ -188,9 +188,9 @@ @@ -299,9 +299,9 @@ diff --git a/map/app/public/js/admin.js b/map/app/public/js/admin.js index 85edc85..a389dc5 100644 --- a/map/app/public/js/admin.js +++ b/map/app/public/js/admin.js @@ -260,6 +260,18 @@ function setupEventListeners() { if (clearShiftBtn) { clearShiftBtn.addEventListener('click', clearShiftForm); } + + // User form submission + const userForm = document.getElementById('user-form'); + if (userForm) { + userForm.addEventListener('submit', createUser); + } + + // Clear user form button + const clearUserBtn = document.getElementById('clear-user-form'); + if (clearUserBtn) { + clearUserBtn.addEventListener('click', clearUserForm); + } } // Setup navigation between admin sections @@ -297,6 +309,12 @@ function setupNavigation() { loadAdminShifts(); } + // If switching to users section, load users + if (targetId === 'users') { + console.log('Loading users...'); + loadUsers(); + } + // If switching to walk sheet section, load config if (targetId === 'walk-sheet') { loadWalkSheetConfig().then((success) => { @@ -537,9 +555,9 @@ function generateWalkSheetPreview() {
- S - M + R L + U
@@ -955,62 +973,6 @@ function displayAdminShifts(shifts) { setupShiftActionListeners(); } -// Fix the setupNavigation function to properly load shifts when switching to shifts section -function setupNavigation() { - const navLinks = document.querySelectorAll('.admin-nav a'); - const sections = document.querySelectorAll('.admin-section'); - - navLinks.forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - - // Get target section ID - const targetId = link.getAttribute('href').substring(1); - - // Hide all sections - sections.forEach(section => { - section.style.display = 'none'; - }); - - // Show target section - const targetSection = document.getElementById(targetId); - if (targetSection) { - targetSection.style.display = 'block'; - } - - // Update active nav link - navLinks.forEach(navLink => { - navLink.classList.remove('active'); - }); - link.classList.add('active'); - - // If switching to shifts section, load shifts - if (targetId === 'shifts') { - console.log('Loading admin shifts...'); - loadAdminShifts(); - } - - // If switching to walk sheet section, load config - if (targetId === 'walk-sheet') { - loadWalkSheetConfig().then((success) => { - if (success) { - generateWalkSheetPreview(); - } - }); - } - }); - }); - - // Also check if we're already on the shifts page (via hash) - const hash = window.location.hash; - if (hash === '#shifts') { - const shiftsLink = document.querySelector('.admin-nav a[href="#shifts"]'); - if (shiftsLink) { - shiftsLink.click(); - } - } -} - // Fix the setupShiftActionListeners function function setupShiftActionListeners() { const list = document.getElementById('admin-shifts-list'); @@ -1112,101 +1074,181 @@ function clearShiftForm() { } } -// Update setupEventListeners to include shift form and clear button -function setupEventListeners() { - // Use current view button - const useCurrentViewBtn = document.getElementById('use-current-view'); - if (useCurrentViewBtn) { - useCurrentViewBtn.addEventListener('click', () => { - const center = adminMap.getCenter(); - const zoom = adminMap.getZoom(); - - document.getElementById('start-lat').value = center.lat.toFixed(6); - document.getElementById('start-lng').value = center.lng.toFixed(6); - document.getElementById('start-zoom').value = zoom; - - updateStartMarker(center.lat, center.lng); - showStatus('Captured current map view', 'success'); - }); - } +// User Management Functions +async function loadUsers() { + const loadingEl = document.getElementById('users-loading'); + const emptyEl = document.getElementById('users-empty'); + const tableBody = document.getElementById('users-table-body'); - // Save button - const saveLocationBtn = document.getElementById('save-start-location'); - if (saveLocationBtn) { - saveLocationBtn.addEventListener('click', saveStartLocation); - } + if (loadingEl) loadingEl.style.display = 'block'; + if (emptyEl) emptyEl.style.display = 'none'; + if (tableBody) tableBody.innerHTML = ''; - // Coordinate input changes - const startLatInput = document.getElementById('start-lat'); - const startLngInput = document.getElementById('start-lng'); - const startZoomInput = document.getElementById('start-zoom'); - - if (startLatInput) startLatInput.addEventListener('change', updateMapFromInputs); - if (startLngInput) startLngInput.addEventListener('change', updateMapFromInputs); - if (startZoomInput) startZoomInput.addEventListener('change', updateMapFromInputs); - - // Walk Sheet buttons - const saveWalkSheetBtn = document.getElementById('save-walk-sheet'); - const previewWalkSheetBtn = document.getElementById('preview-walk-sheet'); - const printWalkSheetBtn = document.getElementById('print-walk-sheet'); - const refreshPreviewBtn = document.getElementById('refresh-preview'); - - if (saveWalkSheetBtn) saveWalkSheetBtn.addEventListener('click', saveWalkSheetConfig); - if (previewWalkSheetBtn) previewWalkSheetBtn.addEventListener('click', generateWalkSheetPreview); - if (printWalkSheetBtn) printWalkSheetBtn.addEventListener('click', printWalkSheet); - if (refreshPreviewBtn) refreshPreviewBtn.addEventListener('click', generateWalkSheetPreview); - - // Auto-update preview on input change - const walkSheetInputs = document.querySelectorAll( - '#walk-sheet-title, #walk-sheet-subtitle, #walk-sheet-footer, ' + - '[id^="qr-code-"][id$="-url"], [id^="qr-code-"][id$="-label"]' - ); - - walkSheetInputs.forEach(input => { - if (input) { - input.addEventListener('input', debounce(() => { - generateWalkSheetPreview(); - }, 500)); + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (loadingEl) loadingEl.style.display = 'none'; + + if (data.success && data.users) { + displayUsers(data.users); + } else { + throw new Error(data.error || 'Failed to load users'); } - }); - - // Add URL change listeners to detect when QR codes need regeneration - for (let i = 1; i <= 3; i++) { - const urlInput = document.getElementById(`qr-code-${i}-url`); - if (urlInput) { - let previousUrl = urlInput.value; - - urlInput.addEventListener('change', () => { - const currentUrl = urlInput.value; - if (currentUrl !== previousUrl) { - console.log(`QR Code ${i} URL changed from "${previousUrl}" to "${currentUrl}"`); - // Remove stored QR code so it gets regenerated - delete storedQRCodes[currentUrl]; - previousUrl = currentUrl; - generateWalkSheetPreview(); - } - }); + + } catch (error) { + console.error('Error loading users:', error); + if (loadingEl) loadingEl.style.display = 'none'; + if (emptyEl) { + emptyEl.textContent = 'Failed to load users'; + emptyEl.style.display = 'block'; } - } - - // Shift form submission - const shiftForm = document.getElementById('shift-form'); - if (shiftForm) { - shiftForm.addEventListener('submit', createShift); - } - - // Clear shift form button - const clearShiftBtn = document.getElementById('clear-shift-form'); - if (clearShiftBtn) { - clearShiftBtn.addEventListener('click', clearShiftForm); + showStatus('Failed to load users', 'error'); } } -// Add the missing clearShiftForm function -function clearShiftForm() { - const form = document.getElementById('shift-form'); - if (form) { - form.reset(); - showStatus('Form cleared', 'info'); +function displayUsers(users) { + const tableBody = document.getElementById('users-table-body'); + const emptyEl = document.getElementById('users-empty'); + + if (!tableBody) return; + + if (!users || users.length === 0) { + if (emptyEl) emptyEl.style.display = 'block'; + return; + } + + if (emptyEl) emptyEl.style.display = 'none'; + + tableBody.innerHTML = users.map(user => { + const createdDate = user.created_at || user['Created At'] || user.createdAt; + const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A'; + const isAdmin = user.admin || user.Admin || false; + const userId = user.Id || user.id || user.ID; + + return ` + + ${escapeHtml(user.email || user.Email || 'N/A')} + ${escapeHtml(user.name || user.Name || 'N/A')} + + + ${isAdmin ? 'Admin' : 'User'} + + + ${formattedDate} + +
+ +
+ + + `; + }).join(''); + + // Setup event listeners for user actions + setupUserActionListeners(); +} + +function setupUserActionListeners() { + const tableBody = document.getElementById('users-table-body'); + if (!tableBody) return; + + // Remove existing listeners by cloning the node + const newTableBody = tableBody.cloneNode(true); + tableBody.parentNode.replaceChild(newTableBody, tableBody); + + // Get the updated reference + const updatedTableBody = document.getElementById('users-table-body'); + + updatedTableBody.addEventListener('click', function(e) { + if (e.target.classList.contains('delete-user-btn')) { + const userId = e.target.getAttribute('data-user-id'); + const userEmail = e.target.getAttribute('data-user-email'); + console.log('Delete button clicked for user:', userId); + deleteUser(userId, userEmail); + } + }); +} + +async function deleteUser(userId, userEmail) { + if (!confirm(`Are you sure you want to delete user "${userEmail}"? This action cannot be undone.`)) { + return; + } + + try { + const response = await fetch(`/api/users/${userId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + showStatus(`User "${userEmail}" deleted successfully`, 'success'); + loadUsers(); // Reload the users list + } else { + throw new Error(data.error || 'Failed to delete user'); + } + + } catch (error) { + console.error('Error deleting user:', error); + showStatus(`Failed to delete user: ${error.message}`, 'error'); + } +} + +async function createUser(e) { + e.preventDefault(); + + const email = document.getElementById('user-email').value.trim(); + const password = document.getElementById('user-password').value; + const name = document.getElementById('user-name').value.trim(); + const admin = document.getElementById('user-admin').checked; + + if (!email || !password) { + showStatus('Email and password are required', 'error'); + return; + } + + if (password.length < 6) { + showStatus('Password must be at least 6 characters long', 'error'); + return; + } + + try { + const response = await fetch('/api/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email, + password, + name: name || '', + admin + }) + }); + + const data = await response.json(); + + if (data.success) { + showStatus('User created successfully', 'success'); + clearUserForm(); + loadUsers(); // Reload the users list + } else { + throw new Error(data.error || 'Failed to create user'); + } + + } catch (error) { + console.error('Error creating user:', error); + showStatus(`Failed to create user: ${error.message}`, 'error'); + } +} + +function clearUserForm() { + const form = document.getElementById('user-form'); + if (form) { + form.reset(); + showStatus('User form cleared', 'info'); } } diff --git a/map/app/public/js/user.js b/map/app/public/js/user.js new file mode 100644 index 0000000..88bfeae --- /dev/null +++ b/map/app/public/js/user.js @@ -0,0 +1,97 @@ +// User profile JavaScript + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + checkUserAuth(); + loadUserProfile(); + setupEventListeners(); +}); + +// Check if user is authenticated +async function checkUserAuth() { + try { + const response = await fetch('/api/auth/check'); + const data = await response.json(); + + if (!data.authenticated) { + window.location.href = '/login.html'; + return; + } + + // Update user info in header + const userInfo = document.getElementById('user-info'); + if (userInfo && data.user) { + userInfo.textContent = `${data.user.name || data.user.email}`; + } + + } catch (error) { + console.error('Auth check failed:', error); + window.location.href = '/login.html'; + } +} + +// Load user profile information +async function loadUserProfile() { + try { + const response = await fetch('/api/auth/check'); + const data = await response.json(); + + if (data.authenticated && data.user) { + document.getElementById('profile-email').textContent = data.user.email || 'N/A'; + document.getElementById('profile-name').textContent = data.user.name || 'N/A'; + document.getElementById('profile-role').textContent = data.user.isAdmin ? 'Administrator' : 'User'; + } + + } catch (error) { + console.error('Failed to load profile:', error); + showStatus('Failed to load profile information', 'error'); + } +} + +// Setup event listeners +function setupEventListeners() { + // Logout button + const logoutBtn = document.getElementById('logout-btn'); + if (logoutBtn) { + logoutBtn.addEventListener('click', handleLogout); + } +} + +// Handle logout +async function handleLogout() { + try { + const response = await fetch('/api/auth/logout', { + method: 'POST', + credentials: 'include' + }); + + if (response.ok) { + window.location.href = '/login.html'; + } else { + throw new Error('Logout failed'); + } + + } catch (error) { + console.error('Logout error:', error); + showStatus('Logout failed', 'error'); + } +} + +// Show status message +function showStatus(message, type = 'info') { + const container = document.getElementById('status-container'); + if (!container) return; + + const statusDiv = document.createElement('div'); + statusDiv.className = `status-message status-${type}`; + statusDiv.textContent = message; + + container.appendChild(statusDiv); + + // Auto remove after 5 seconds + setTimeout(() => { + if (statusDiv.parentNode) { + statusDiv.parentNode.removeChild(statusDiv); + } + }, 5000); +} diff --git a/map/app/public/user.html b/map/app/public/user.html new file mode 100644 index 0000000..d48d78e --- /dev/null +++ b/map/app/public/user.html @@ -0,0 +1,59 @@ + + + + + + + User Profile + + + + + + + + + + +
+ +
+

User Profile

+
+ ← Back to Map + + +
+
+ + +
+ +
+ + +
+
+ + + + + diff --git a/map/app/routes/admin.js b/map/app/routes/admin.js index 3083696..0210eec 100644 --- a/map/app/routes/admin.js +++ b/map/app/routes/admin.js @@ -2,6 +2,26 @@ const express = require('express'); const router = express.Router(); const settingsController = require('../controllers/settingsController'); +// Debug endpoint to check configuration +router.get('/config-debug', (req, res) => { + const config = require('../config'); + res.json({ + success: true, + config: { + nocodb: { + apiUrl: config.nocodb.apiUrl, + hasToken: !!config.nocodb.apiToken, + projectId: config.nocodb.projectId, + tableId: config.nocodb.tableId, + loginSheetId: config.nocodb.loginSheetId, + settingsSheetId: config.nocodb.settingsSheetId, + shiftsSheetId: config.nocodb.shiftsSheetId, + shiftSignupsSheetId: config.nocodb.shiftSignupsSheetId + } + } + }); +}); + // Start location management router.get('/start-location', settingsController.getStartLocation); router.post('/start-location', settingsController.updateStartLocation); diff --git a/map/app/routes/debug.js b/map/app/routes/debug.js index 362383b..3276851 100644 --- a/map/app/routes/debug.js +++ b/map/app/routes/debug.js @@ -222,4 +222,43 @@ router.get('/walk-sheet-raw', async (req, res) => { } }); +// Add this route to check login table structure +router.get('/login-structure', async (req, res) => { + try { + const loginSheetId = config.nocodb.loginSheetId; + + if (!loginSheetId) { + return res.status(400).json({ + success: false, + error: 'Login sheet ID not configured' + }); + } + + // Get table structure + const tableId = extractTableId(loginSheetId); + const response = await nocodbService.api.get(`/db/meta/tables/${tableId}`); + + const columns = response.data.columns.map(col => ({ + column_name: col.column_name, + title: col.title, + uidt: col.uidt, + required: col.rqd + })); + + res.json({ + success: true, + tableId, + columns, + columnNames: columns.map(c => c.column_name) + }); + + } catch (error) { + logger.error('Error fetching login table structure:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/map/app/routes/index.js b/map/app/routes/index.js index 51b37a0..2eb0219 100644 --- a/map/app/routes/index.js +++ b/map/app/routes/index.js @@ -102,6 +102,11 @@ module.exports = (app) => { res.sendFile(path.join(__dirname, '../public', 'shifts.html')); }); + // User profile page route + app.get('/user.html', requireAuth, (req, res) => { + res.sendFile(path.join(__dirname, '../public', 'user.html')); + }); + // Catch all - redirect to login app.get('*', (req, res) => { res.redirect('/login.html'); diff --git a/map/build-nocodb.md b/map/build-nocodb.md index d97b586..8392110 100644 --- a/map/build-nocodb.md +++ b/map/build-nocodb.md @@ -344,7 +344,7 @@ Updated the build-nocodb.sh script to use proper NocoDB column types based on th - `unit_number` (SingleLineText) - `support_level` (SingleSelect with colors: 1=Green, 2=Yellow, 3=Orange, 4=Red) - `sign` (Checkbox) - - `sign_size` (SingleSelect: Small, Medium, Large) + - `sign_size` (SingleSelect: Regular, Large, Unsure) - `notes` (LongText) - `address` (SingleLineText instead of LongText) diff --git a/map/build-nocodb.sh b/map/build-nocodb.sh index e7c7666..785f6f9 100755 --- a/map/build-nocodb.sh +++ b/map/build-nocodb.sh @@ -343,9 +343,9 @@ create_locations_table() { "rqd": false, "colOptions": { "options": [ - {"title": "Small", "color": "#2196F3"}, - {"title": "Medium", "color": "#FF9800"}, - {"title": "Large", "color": "#4CAF50"} + {"title": "Regular", "color": "#2196F3"}, + {"title": "Large", "color": "#4CAF50"}, + {"title": "Unsure", "color": "#FF9800"} ] } }, diff --git a/map/files-explainer.md b/map/files-explainer.md index 63e045a..5bfd473 100644 --- a/map/files-explainer.md +++ b/map/files-explainer.md @@ -92,7 +92,7 @@ Winston logger configuration for backend logging. # app/public/admin.html -Admin panel HTML page for managing start location, walk sheet, and settings. +Admin panel HTML page for managing start location, walk sheet, shift management, and user management. # app/public/css/admin.css @@ -122,9 +122,21 @@ Login page HTML for user authentication. Volunteer shifts management and signup page HTML with both grid and calendar view options. +# app/public/user.html + +User profile page HTML for displaying user information and account management. + +# app/public/css/user.css + +CSS styles for the user profile page and user management components in the admin panel. + # app/public/js/admin.js -JavaScript for admin panel functionality (map, start location, walk sheet, etc). +JavaScript for admin panel functionality (map, start location, walk sheet, shift management, and user management). + +# app/public/js/user.js + +JavaScript for user profile page functionality and user account management. # app/public/js/auth.js diff --git a/mkdocs/.cache/plugin/social/7ca622286d4c40d181cd6c809308aadd.png b/mkdocs/.cache/plugin/social/7ca622286d4c40d181cd6c809308aadd.png new file mode 100644 index 0000000..5dd10c6 Binary files /dev/null and b/mkdocs/.cache/plugin/social/7ca622286d4c40d181cd6c809308aadd.png differ diff --git a/mkdocs/.cache/plugin/social/a0553b3e88ffc3c868350353b63036cb.png b/mkdocs/.cache/plugin/social/a0553b3e88ffc3c868350353b63036cb.png new file mode 100644 index 0000000..441516d Binary files /dev/null and b/mkdocs/.cache/plugin/social/a0553b3e88ffc3c868350353b63036cb.png differ diff --git a/mkdocs/docs/config/map.md b/mkdocs/docs/config/map.md index 113b39c..009116e 100644 --- a/mkdocs/docs/config/map.md +++ b/mkdocs/docs/config/map.md @@ -133,7 +133,7 @@ Main table for storing map data: - 3 = Low Support (Orange) - 4 = No Support (Red) - `Sign` (Checkbox): Has campaign sign -- `Sign Size` (Single Select): Options: "Small", "Medium", "Large" +- `Sign Size` (Single Select): Options: "Regular", "Large", "Unsure" - `Notes` (Long Text): Additional details and comments #### 2. Login Table diff --git a/mkdocs/docs/how to/canvass.md b/mkdocs/docs/how to/canvass.md new file mode 100644 index 0000000..553f7d5 --- /dev/null +++ b/mkdocs/docs/how to/canvass.md @@ -0,0 +1,4 @@ +# Canvas + +This is BNKops canvassing how to! In the following document, you will find all sorts of tips and tricks for door knocking, canvassing, and using the BNKops canvassing app. + diff --git a/mkdocs/docs/manual/map.md b/mkdocs/docs/manual/map.md index 6699944..df924a1 100644 --- a/mkdocs/docs/manual/map.md +++ b/mkdocs/docs/manual/map.md @@ -2,7 +2,8 @@ Quick Tips: -- Map works best when you clear your cookies, cache, and other data before use! This is because it is a web-app that pushes information to your phone. By clearing that data, you will always load the most recent version of the app to your browser. +- **Data:** Map works best when you clear your cookies, cache, and other data before use! This is because it is a web-app that pushes information to your phone. By clearing that data, you will always load the most recent version of the app to your browser. +- **Browser:** Map will work on nearly any browser however the developers test on Firefox, Brave, & Chromium. Firefox is what the bnkops team uses to access Map and is generally the most stable. ## How to add new location - Video diff --git a/mkdocs/site/config/map/index.html b/mkdocs/site/config/map/index.html index 99920b5..ddb613c 100644 --- a/mkdocs/site/config/map/index.html +++ b/mkdocs/site/config/map/index.html @@ -2629,7 +2629,7 @@ Changemaker Archive. Learn more
  • 3 = Low Support (Orange)
  • 4 = No Support (Red)
  • Sign (Checkbox): Has campaign sign
  • -
  • Sign Size (Single Select): Options: "Small", "Medium", "Large"
  • +
  • Sign Size (Single Select): Options: "Regular", "Large", "Unsure"
  • Notes (Long Text): Additional details and comments
  • 2. Login Table

    diff --git a/mkdocs/site/services/map/index.html b/mkdocs/site/services/map/index.html index 1e1167a..da5164d 100644 --- a/mkdocs/site/services/map/index.html +++ b/mkdocs/site/services/map/index.html @@ -1873,7 +1873,7 @@ Changemaker Archive. Learn more
  • 4 - No Support (Red)
  • Address (Text): Full street address
  • Sign (Checkbox): Has campaign sign (true/false)
  • -
  • Sign Size (Single Select): Small, Medium, Large
  • +
  • Sign Size (Single Select): Regular, Large, Unsure
  • Geo-Location (Text): Formatted as "latitude;longitude"
  • API Endpoints