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() {
Sign (Checkbox): Has campaign signSign Size (Single Select): Options: "Small", "Medium", "Large"Sign Size (Single Select): Options: "Regular", "Large", "Unsure"Notes (Long Text): Additional details and commentsAddress (Text): Full street addressSign (Checkbox): Has campaign sign (true/false)Sign Size (Single Select): Small, Medium, LargeSign Size (Single Select): Regular, Large, UnsureGeo-Location (Text): Formatted as "latitude;longitude"