From 56b1600c37fd607d864f7503126a1193e595c99d Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 10 Sep 2025 12:33:55 -0600 Subject: [PATCH] Fixed some bugs with menus and updated the build-nocodb to migrate data. --- map/README.md | 48 ++ map/app/controllers/usersController.js | 6 +- map/app/public/admin.html | 14 +- map/app/public/css/admin/cuts-shifts.css | 16 + map/app/public/css/admin/layout.css | 83 ++- map/app/public/css/admin/responsive.css | 130 ++++- map/app/public/css/admin/user-management.css | 124 ++++ map/app/public/js/admin-core.js | 137 ++++- map/app/public/js/admin-shift-volunteers.js | 165 +++++- map/app/public/js/admin-shifts.js | 17 +- map/app/public/js/admin-users.js | 36 +- map/build-nocodb.sh | 582 ++++++++++++++++++- map/files-explainer.md | 9 +- map/test_print_debug.html | 89 --- 14 files changed, 1276 insertions(+), 180 deletions(-) delete mode 100644 map/test_print_debug.html diff --git a/map/README.md b/map/README.md index 6917d9f..d88c17b 100644 --- a/map/README.md +++ b/map/README.md @@ -143,6 +143,11 @@ ./build-nocodb.sh ``` + **For migrating from an existing NocoDB base:** + ```bash + ./build-nocodb.sh --migrate-data + ``` + This creates six tables: - **Locations** - Main map data with geo-location, contact info, support levels - **Login** - User authentication (email, name, admin flag) @@ -151,6 +156,49 @@ - **Shift Signups** - User shift registrations - **Cuts** - Geographic polygon overlays for map regions + ### Data Migration Options + + The build script supports data migration from existing NocoDB bases: + + **Interactive Mode (Default):** + ```bash + ./build-nocodb.sh + ``` + - Prompts you to choose between fresh installation or data migration + - Automatically detects current base from .env file + - Provides guided setup with clear options + + **Fresh Installation:** + - Creates new base with sample data + - Sets up default admin user (admin@thebunkerops.ca / admin123) + - Configures default settings + + **Migration from Existing Base:** + ```bash + ./build-nocodb.sh --migrate-data # Skip prompt, go direct to migration + ``` + - Lists all available bases in your NocoDB instance + - Highlights current base from .env file for easy selection + - Allows you to select source base for migration + - Choose specific tables to migrate (locations, login, settings, etc.) + - Filters out auto-generated columns to prevent conflicts + - Preserves your existing data while updating to new schema + - Original base remains unchanged as backup + + **Migration Process:** + 1. Script displays available bases with IDs and descriptions + 2. Select source base by entering the corresponding number + 3. Choose tables to migrate (comma-separated numbers or 'all') + 4. Data is exported from source and imported to new base + 5. .env file automatically updated with new URLs + + **Important Migration Notes:** + - ✅ Original data remains untouched (creates new base) + - ✅ Auto-generates new IDs to prevent conflicts + - ✅ Validates table structure compatibility + - ⚠️ Review migrated data before using in production + - ⚠️ Existing admin passwords may need to be reset + 4. **Get Table URLs** After the script completes: diff --git a/map/app/controllers/usersController.js b/map/app/controllers/usersController.js index 2b389fb..799eca5 100644 --- a/map/app/controllers/usersController.js +++ b/map/app/controllers/usersController.js @@ -56,7 +56,7 @@ class UsersController { async create(req, res) { try { - const { email, password, name, isAdmin, userType, expireDays } = req.body; + const { email, password, name, phone, isAdmin, userType, expireDays } = req.body; if (!email || !password) { return res.status(400).json({ @@ -98,6 +98,8 @@ class UsersController { password: password, Name: name || '', name: name || '', + Phone: phone || '', + phone: phone || '', Admin: isAdmin === true, admin: isAdmin === true, 'User Type': userType || 'user', // Handle space in field name @@ -121,6 +123,7 @@ class UsersController { ID: extractId(response), Email: email, Name: name, + Phone: phone, Admin: isAdmin, 'User Type': userType, // Handle space in field name UserType: userType, @@ -157,6 +160,7 @@ class UsersController { id: extractId(response), email: email, name: name, + phone: phone, admin: isAdmin, userType: userType, expiresAt: expiresAt diff --git a/map/app/public/admin.html b/map/app/public/admin.html index 0f7c240..2f741a2 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -23,10 +23,12 @@
-

Admin Panel

@@ -590,6 +592,10 @@
+
+ + +
diff --git a/map/app/public/css/admin/cuts-shifts.css b/map/app/public/css/admin/cuts-shifts.css index df9524d..c0eea30 100644 --- a/map/app/public/css/admin/cuts-shifts.css +++ b/map/app/public/css/admin/cuts-shifts.css @@ -298,6 +298,22 @@ color: var(--secondary-color); } +/* Volunteer Names Display */ +.volunteer-names { + font-size: 0.85em; + color: var(--primary-color); + font-weight: normal; + opacity: 0.8; + font-style: italic; +} + +.volunteer-count { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + /* Shift Status Colors */ .status-open { color: var(--success-color); diff --git a/map/app/public/css/admin/layout.css b/map/app/public/css/admin/layout.css index df2e577..60ba838 100644 --- a/map/app/public/css/admin/layout.css +++ b/map/app/public/css/admin/layout.css @@ -154,35 +154,90 @@ /* Mobile Menu Components */ .mobile-menu-toggle { display: none; - background: none; - border: none; + background: transparent; + border: 2px solid rgba(255, 255, 255, 0.8); + border-radius: 4px; padding: 8px; cursor: pointer; position: relative; - width: 40px; - height: 40px; + width: 44px; + height: 44px; + z-index: 10002; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + flex-shrink: 0; + min-width: 44px; + min-height: 44px; + -webkit-appearance: none; + appearance: none; + outline: none; + isolation: isolate; + transition: all 0.3s ease; } -.mobile-menu-toggle span { - display: block; +/* Hamburger Icon */ +.hamburger-icon { + display: flex; + flex-direction: column; + justify-content: space-around; width: 24px; + height: 20px; + position: relative; +} + +.hamburger-icon span { + display: block; height: 3px; - background: white; - margin: 5px auto; - transition: var(--transition); + width: 100%; + background-color: white; border-radius: 2px; + transition: all 0.3s ease; } -.mobile-menu-toggle.active span:nth-child(1) { - transform: rotate(45deg) translate(5px, 5px); +/* Active state animation */ +.mobile-menu-toggle.active .hamburger-icon span:nth-child(1) { + transform: translateY(7px) rotate(45deg); } -.mobile-menu-toggle.active span:nth-child(2) { +.mobile-menu-toggle.active .hamburger-icon span:nth-child(2) { opacity: 0; } -.mobile-menu-toggle.active span:nth-child(3) { - transform: rotate(-45deg) translate(7px, -6px); +.mobile-menu-toggle.active .hamburger-icon span:nth-child(3) { + transform: translateY(-7px) rotate(-45deg); +} + +/* Hover effect */ +.mobile-menu-toggle:hover { + background-color: rgba(255, 255, 255, 0.1); + border-color: white; +} + +/* Ensure button is visible on mobile */ +@media (max-width: 768px) { + .mobile-menu-toggle { + display: flex !important; + align-items: center; + justify-content: center; + margin-right: 10px; + } + + /* Make sure header has proper layout on mobile */ + .header { + display: flex; + align-items: center; + position: relative; + z-index: 10001; + padding: 10px 15px; + } + + .header h1 { + margin-left: 10px; + font-size: 1.5rem; + } } /* Sidebar Header & Footer (mobile) */ diff --git a/map/app/public/css/admin/responsive.css b/map/app/public/css/admin/responsive.css index d748ab9..35f02cf 100644 --- a/map/app/public/css/admin/responsive.css +++ b/map/app/public/css/admin/responsive.css @@ -50,7 +50,51 @@ @media (max-width: 768px) { /* Show mobile menu toggle */ .mobile-menu-toggle { - display: block; + display: flex !important; + } + + /* Sidebar as overlay */ + .admin-sidebar { + position: fixed !important; + top: var(--header-height, 60px); + left: -300px; /* Changed from -100% to fixed value that's larger than width */ + width: 280px !important; + max-width: 80vw !important; + min-width: 250px !important; + height: calc(100vh - var(--header-height, 60px)); + height: calc(var(--app-height, 100vh) - var(--header-height, 60px)); + z-index: 10000; + transition: left 0.3s ease; + box-shadow: 2px 0 10px rgba(0,0,0,0.1); + overflow-y: auto; + transform: translateX(0); /* Ensure no transform issues */ + } + + .admin-sidebar.active { + left: 0 !important; + transform: translateX(0); /* Ensure it's fully visible */ + } + + /* Show mobile sidebar elements */ + .sidebar-header { + display: flex !important; + } + + .sidebar-footer { + display: block !important; + } + + /* Prevent body scroll when sidebar is open */ + body.sidebar-open { + overflow: hidden; + position: fixed; + width: 100%; + } + + /* Admin content takes full width */ + .admin-content { + width: 100%; + margin-left: 0; } /* Header adjustments */ @@ -84,7 +128,9 @@ height: calc(var(--app-height) - 50px); } - /* Sidebar as overlay */ + /* Remove duplicate sidebar styles - keep only the first one above */ + /* DELETE or comment out this duplicate block: */ + /* .admin-sidebar { position: fixed; top: 0; @@ -104,6 +150,7 @@ .admin-sidebar.active { left: 0; } + */ /* Show sidebar header and footer on mobile */ .sidebar-header { @@ -272,6 +319,19 @@ justify-content: flex-end; } + /* Volunteer names mobile styling */ + .volunteer-count { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + + .volunteer-names { + font-size: 0.8em; + line-height: 1.2; + word-break: break-word; + } + /* Walk sheet container mobile */ .walk-sheet-container { display: flex !important; @@ -363,7 +423,23 @@ width: 100%; } - .user-actions .btn { + .user-communication-actions { + justify-content: center; + gap: 8px; + } + + .user-communication-actions .btn { + min-width: 40px; + padding: 8px 12px; + font-size: 16px; + } + + .user-admin-actions { + flex-direction: column; + gap: 6px; + } + + .user-admin-actions .btn { font-size: var(--font-size-xs); padding: 8px 10px; width: 100%; @@ -399,6 +475,24 @@ .volunteer-actions { align-self: flex-end; + gap: 6px; + } + + .volunteer-communication-actions { + justify-content: center; + gap: 8px; + margin-bottom: 6px; + } + + .volunteer-communication-actions .btn { + min-width: 40px; + padding: 8px 12px; + font-size: 16px; + } + + .volunteer-admin-actions { + flex-direction: column; + gap: 6px; } .processing-actions { @@ -409,11 +503,15 @@ /* Very Small Screens (under 480px) */ @media (max-width: 480px) { .admin-sidebar { - width: 260px; - left: -260px; + width: 260px !important; /* Added !important to override */ + left: -280px !important; /* Increased to ensure complete hiding */ padding: 12px; } + .admin-sidebar.active { + left: 0 !important; + } + .admin-nav { gap: 6px; margin: 15px 0; @@ -490,8 +588,12 @@ /* Ultra Small Screens (under 360px) */ @media (max-width: 360px) { .admin-sidebar { - width: 240px; - left: -240px; + width: 240px !important; /* Added !important */ + left: -260px !important; /* Increased to ensure complete hiding */ + } + + .admin-sidebar.active { + left: 0 !important; } .admin-nav a { @@ -681,11 +783,23 @@ .user-actions { justify-content: flex-end; margin-top: 8px; + gap: 6px; } - .user-actions .btn { + .user-communication-actions { + margin-bottom: 6px; + } + + .user-communication-actions .btn { + min-width: 32px; + padding: 6px 8px; + } + + .user-admin-actions .btn { width: auto !important; min-width: 80px; flex: none; + font-size: 10px; + padding: 6px 8px; } } diff --git a/map/app/public/css/admin/user-management.css b/map/app/public/css/admin/user-management.css index 6f6222b..bbc334a 100644 --- a/map/app/public/css/admin/user-management.css +++ b/map/app/public/css/admin/user-management.css @@ -130,9 +130,22 @@ /* User Actions */ .user-actions { display: flex; + flex-direction: column; gap: 8px; } +.user-communication-actions { + display: flex; + gap: 4px; + justify-content: center; +} + +.user-admin-actions { + display: flex; + gap: 8px; + justify-content: center; +} + .user-actions .btn { padding: 6px 12px; font-size: var(--font-size-xs); @@ -140,6 +153,55 @@ font-weight: 500; } +.user-communication-actions .btn { + padding: 4px 8px; + font-size: 14px; + min-width: 32px; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.user-communication-actions .btn-outline-primary { + color: var(--primary-color); + border: 1px solid var(--primary-color); + background: white; +} + +.user-communication-actions .btn-outline-primary:hover { + background: var(--primary-color); + color: white; +} + +.user-communication-actions .btn-outline-secondary { + color: #6c757d; + border: 1px solid #6c757d; + background: white; +} + +.user-communication-actions .btn-outline-secondary:hover:not(.disabled) { + background: #6c757d; + color: white; +} + +.user-communication-actions .btn-outline-success { + color: var(--success-color); + border: 1px solid var(--success-color); + background: white; +} + +.user-communication-actions .btn-outline-success:hover:not(.disabled) { + background: var(--success-color); + color: white; +} + +.user-communication-actions .btn.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + /* Users List Header */ .users-list-header { display: flex; @@ -191,9 +253,71 @@ .volunteer-actions { display: flex; + flex-direction: column; gap: 8px; } +.volunteer-communication-actions { + display: flex; + gap: 4px; + justify-content: center; +} + +.volunteer-admin-actions { + display: flex; + gap: 8px; + justify-content: center; +} + +.volunteer-communication-actions .btn { + padding: 4px 8px; + font-size: 14px; + min-width: 32px; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.volunteer-communication-actions .btn-outline-primary { + color: var(--primary-color); + border: 1px solid var(--primary-color); + background: white; +} + +.volunteer-communication-actions .btn-outline-primary:hover { + background: var(--primary-color); + color: white; +} + +.volunteer-communication-actions .btn-outline-secondary { + color: #6c757d; + border: 1px solid #6c757d; + background: white; +} + +.volunteer-communication-actions .btn-outline-secondary:hover:not(.disabled) { + background: #6c757d; + color: white; +} + +.volunteer-communication-actions .btn-outline-success { + color: var(--success-color); + border: 1px solid var(--success-color); + background: white; +} + +.volunteer-communication-actions .btn-outline-success:hover:not(.disabled) { + background: var(--success-color); + color: white; +} + +.volunteer-communication-actions .btn.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + .no-volunteers { text-align: center; color: #666; diff --git a/map/app/public/js/admin-core.js b/map/app/public/js/admin-core.js index 7860e9a..3794ee4 100644 --- a/map/app/public/js/admin-core.js +++ b/map/app/public/js/admin-core.js @@ -43,49 +43,127 @@ function setAdminViewportDimensions() { // Add mobile menu functionality function setupMobileMenu() { + console.log('🔧 Setting up mobile menu...'); const menuToggle = document.getElementById('mobile-menu-toggle'); const sidebar = document.getElementById('admin-sidebar'); const closeSidebar = document.getElementById('close-sidebar'); const adminNavLinks = document.querySelectorAll('.admin-nav a'); + console.log('📱 Mobile menu elements found:', { + menuToggle: !!menuToggle, + sidebar: !!sidebar, + closeSidebar: !!closeSidebar, + adminNavLinks: adminNavLinks.length + }); + if (menuToggle && sidebar) { - // Toggle menu - menuToggle.addEventListener('click', () => { - sidebar.classList.toggle('active'); - menuToggle.classList.toggle('active'); - document.body.classList.toggle('sidebar-open'); - }); + console.log('✅ Setting up mobile menu event listeners...'); + + // Remove any existing listeners to prevent duplicates + const newMenuToggle = menuToggle.cloneNode(true); + menuToggle.parentNode.replaceChild(newMenuToggle, menuToggle); + + // Toggle menu function + const toggleMobileMenu = (e) => { + console.log('🔄 Mobile menu toggle triggered!', e.type); + e.preventDefault(); + e.stopPropagation(); + + const sidebar = document.getElementById('admin-sidebar'); + const menuToggle = document.getElementById('mobile-menu-toggle'); + + if (!sidebar || !menuToggle) { + console.error('❌ Sidebar or menu toggle not found during toggle'); + return; + } + + const isActive = sidebar.classList.contains('active'); + console.log('📱 Current menu state:', isActive ? 'open' : 'closed'); + + if (isActive) { + sidebar.classList.remove('active'); + menuToggle.classList.remove('active'); + document.body.classList.remove('sidebar-open'); + console.log('✅ Menu closed'); + } else { + sidebar.classList.add('active'); + menuToggle.classList.add('active'); + document.body.classList.add('sidebar-open'); + console.log('✅ Menu opened'); + } + }; + + // Use pointer events for better mobile support + const toggleButton = document.getElementById('mobile-menu-toggle'); + + // Add click event for all devices + toggleButton.addEventListener('click', toggleMobileMenu, { passive: false }); + + // Add pointer events for better mobile support + toggleButton.addEventListener('pointerdown', (e) => { + // Visual feedback + e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'; + }, { passive: true }); + + toggleButton.addEventListener('pointerup', (e) => { + // Remove visual feedback + e.currentTarget.style.backgroundColor = ''; + }, { passive: true }); // Close sidebar button if (closeSidebar) { closeSidebar.addEventListener('click', () => { - sidebar.classList.remove('active'); - menuToggle.classList.remove('active'); - document.body.classList.remove('sidebar-open'); + const sidebar = document.getElementById('admin-sidebar'); + const menuToggle = document.getElementById('mobile-menu-toggle'); + + if (sidebar && menuToggle) { + sidebar.classList.remove('active'); + menuToggle.classList.remove('active'); + document.body.classList.remove('sidebar-open'); + console.log('✅ Sidebar closed via close button'); + } }); } // Close sidebar when clicking outside document.addEventListener('click', (e) => { - if (sidebar.classList.contains('active') && + const sidebar = document.getElementById('admin-sidebar'); + const menuToggle = document.getElementById('mobile-menu-toggle'); + + if (sidebar && menuToggle && sidebar.classList.contains('active') && !sidebar.contains(e.target) && !menuToggle.contains(e.target)) { sidebar.classList.remove('active'); menuToggle.classList.remove('active'); document.body.classList.remove('sidebar-open'); + console.log('✅ Sidebar closed by clicking outside'); } }); // Close sidebar when navigation link is clicked on mobile - adminNavLinks.forEach(link => { + const navLinks = document.querySelectorAll('.admin-nav a'); + navLinks.forEach(link => { link.addEventListener('click', () => { if (window.innerWidth <= 768) { - sidebar.classList.remove('active'); - menuToggle.classList.remove('active'); - document.body.classList.remove('sidebar-open'); + const sidebar = document.getElementById('admin-sidebar'); + const menuToggle = document.getElementById('mobile-menu-toggle'); + + if (sidebar && menuToggle) { + sidebar.classList.remove('active'); + menuToggle.classList.remove('active'); + document.body.classList.remove('sidebar-open'); + console.log('✅ Sidebar closed after navigation'); + } } }); }); + + console.log('✅ Mobile menu setup complete!'); + } else { + console.error('❌ Mobile menu elements not found:', { + menuToggle: !!menuToggle, + sidebar: !!sidebar + }); } } @@ -371,29 +449,42 @@ async function loadDashboardDataFromDashboardModule() { // Initialize the admin core when DOM is loaded function initializeAdminCore() { + console.log('🚀 Initializing Admin Core...'); + // Set initial viewport dimensions and listen for resize events setAdminViewportDimensions(); window.addEventListener('resize', setAdminViewportDimensions); window.addEventListener('orientationchange', () => { - // Add a small delay for orientation change to complete setTimeout(setAdminViewportDimensions, 100); }); + // Setup navigation first setupNavigation(); - setupMobileMenu(); + + // Setup mobile menu with a small delay to ensure DOM is ready + setTimeout(() => { + setupMobileMenu(); + }, 100); // Check if URL has a hash to show specific section const hash = window.location.hash; if (hash === '#walk-sheet') { + console.log('Direct navigation to walk-sheet section'); showSection('walk-sheet'); - } else if (hash === '#convert-data') { - showSection('convert-data'); - } else if (hash === '#cuts') { - showSection('cuts'); - } else { - // Default to dashboard - showSection('dashboard'); + } else if (hash) { + const sectionId = hash.substring(1); + showSection(sectionId); } + + console.log('✅ Admin Core initialized'); +} + +// Make sure we wait for DOM to be fully loaded +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeAdminCore); +} else { + // DOM is already loaded + initializeAdminCore(); } // Export functions for use by other modules diff --git a/map/app/public/js/admin-shift-volunteers.js b/map/app/public/js/admin-shift-volunteers.js index c264433..c4db8a9 100644 --- a/map/app/public/js/admin-shift-volunteers.js +++ b/map/app/public/js/admin-shift-volunteers.js @@ -69,21 +69,13 @@ function populateUserSelect() { async function showShiftUserModal(shiftId, shiftData) { currentShiftData = { ...shiftData, ID: shiftId }; - // Update modal title and info - const modalTitle = document.getElementById('modal-shift-title'); - const modalDetails = document.getElementById('modal-shift-details'); - - if (modalTitle) modalTitle.textContent = shiftData.Title; - - if (modalDetails) { - const shiftDate = safeAdminCore('createLocalDate', shiftData.Date) || new Date(shiftData.Date); - modalDetails.textContent = - `${shiftDate.toLocaleDateString()} | ${shiftData['Start Time']} - ${shiftData['End Time']} | ${shiftData.Location || 'TBD'}`; - } + // Update modal title and info using the new function + updateModalTitle(); // Load users if not already loaded if (allUsers.length === 0) { await loadAllUsers(); + populateUserSelect(); } // Display current volunteers @@ -114,11 +106,32 @@ function displayCurrentVolunteers(volunteers) {
${safeAdminCore('escapeHtml', volunteer['User Email']) || volunteer['User Email'] || ''}
- +
+ + 📧 + + + +
+
+ +
`).join(''); @@ -137,6 +150,14 @@ function setupVolunteerActionListeners() { const volunteerId = e.target.getAttribute('data-volunteer-id'); const volunteerEmail = e.target.getAttribute('data-volunteer-email'); removeVolunteerFromShift(volunteerId, volunteerEmail); + } else if (e.target.classList.contains('sms-volunteer-btn')) { + const volunteerEmail = e.target.getAttribute('data-volunteer-email'); + const volunteerName = e.target.getAttribute('data-volunteer-name'); + openVolunteerSMS(volunteerEmail, volunteerName); + } else if (e.target.classList.contains('call-volunteer-btn')) { + const volunteerEmail = e.target.getAttribute('data-volunteer-email'); + const volunteerName = e.target.getAttribute('data-volunteer-name'); + callVolunteer(volunteerEmail, volunteerName); } }); } @@ -177,6 +198,9 @@ async function addUserToShift() { try { await refreshCurrentShiftData(); console.log('Refreshed shift data after adding user'); + + // Also update the modal title to reflect new volunteer count + updateModalTitle(); } catch (refreshError) { console.error('Error during refresh after adding user:', refreshError); // Still show success since the add operation worked @@ -216,6 +240,9 @@ async function removeVolunteerFromShift(volunteerId, volunteerEmail) { try { await refreshCurrentShiftData(); console.log('Refreshed shift data after removing volunteer'); + + // Also update the modal title to reflect new volunteer count + updateModalTitle(); } catch (refreshError) { console.error('Error during refresh after removing volunteer:', refreshError); // Still show success since the remove operation worked @@ -272,13 +299,27 @@ function updateShiftInList(updatedShift) { if (shiftItem) { const signupCount = updatedShift.signups ? updatedShift.signups.length : 0; + // Generate list of first names for volunteers (same logic as displayAdminShifts) + const firstNames = updatedShift.signups ? updatedShift.signups.map(volunteer => { + const fullName = volunteer['User Name'] || volunteer['User Email'] || 'Unknown'; + // Extract first name (everything before first space, or email username if no space) + const firstName = fullName.includes(' ') ? fullName.split(' ')[0] : + fullName.includes('@') ? fullName.split('@')[0] : fullName; + return safeAdminCore('escapeHtml', firstName) || firstName; + }).slice(0, 8) : []; // Limit to first 8 names to avoid overflow + + const namesDisplay = firstNames.length > 0 ? + `(${firstNames.join(', ')}${firstNames.length === 8 && signupCount > 8 ? '...' : ''})` : + ''; + // Find the volunteer count paragraph (contains 👥) const volunteerCountElement = Array.from(shiftItem.querySelectorAll('p')).find(p => - p.textContent.includes('👥') + p.textContent.includes('👥') || p.classList.contains('volunteer-count') ); if (volunteerCountElement) { - volunteerCountElement.textContent = `👥 ${signupCount}/${updatedShift['Max Volunteers']} volunteers`; + volunteerCountElement.innerHTML = `👥 ${signupCount}/${updatedShift['Max Volunteers']} volunteers ${namesDisplay}`; + volunteerCountElement.className = 'volunteer-count'; // Ensure class is set } // Update the data attribute with new shift data @@ -290,6 +331,32 @@ function updateShiftInList(updatedShift) { } } +// Update modal title with current volunteer count +function updateModalTitle() { + if (!currentShiftData) return; + + const modalTitle = document.getElementById('modal-shift-title'); + const modalDetails = document.getElementById('modal-shift-details'); + + if (modalTitle) { + const signupCount = currentShiftData.signups ? currentShiftData.signups.length : 0; + modalTitle.textContent = `Manage Volunteers - ${currentShiftData.Title} (${signupCount}/${currentShiftData['Max Volunteers']})`; + } + + if (modalDetails) { + const shiftDate = safeAdminCore('createLocalDate', currentShiftData.Date); + const dateStr = shiftDate ? shiftDate.toLocaleDateString() : currentShiftData.Date; + const signupCount = currentShiftData.signups ? currentShiftData.signups.length : 0; + + modalDetails.innerHTML = ` +

Date: ${dateStr}

+

Time: ${currentShiftData['Start Time']} - ${currentShiftData['End Time']}

+

Location: ${safeAdminCore('escapeHtml', currentShiftData.Location || 'TBD') || currentShiftData.Location || 'TBD'}

+

Current Signups: ${signupCount} / ${currentShiftData['Max Volunteers']}

+ `; + } +} + // Close modal function closeShiftUserModal() { const modal = document.getElementById('shift-user-modal'); @@ -303,6 +370,61 @@ function closeShiftUserModal() { console.log('Modal closed - shifts list should already be current'); } +// Communication functions for individual volunteers +async function openVolunteerSMS(volunteerEmail, volunteerName) { + try { + // Look up the volunteer's phone number from the users database + const user = await getUserByEmail(volunteerEmail); + + if (user && (user.phone || user.Phone)) { + const phoneNumber = user.phone || user.Phone; + const smsUrl = `sms:${phoneNumber}`; + window.open(smsUrl, '_self'); + } else { + safeAdminCore('showStatus', `No phone number found for ${volunteerName}`, 'warning'); + } + } catch (error) { + console.error('Error looking up volunteer phone number:', error); + safeAdminCore('showStatus', 'Failed to lookup volunteer phone number', 'error'); + } +} + +async function callVolunteer(volunteerEmail, volunteerName) { + try { + // Look up the volunteer's phone number from the users database + const user = await getUserByEmail(volunteerEmail); + + if (user && (user.phone || user.Phone)) { + const phoneNumber = user.phone || user.Phone; + const telUrl = `tel:${phoneNumber}`; + window.open(telUrl, '_self'); + } else { + safeAdminCore('showStatus', `No phone number found for ${volunteerName}`, 'warning'); + } + } catch (error) { + console.error('Error looking up volunteer phone number:', error); + safeAdminCore('showStatus', 'Failed to lookup volunteer phone number', 'error'); + } +} + +// Helper function to get user details by email +async function getUserByEmail(email) { + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (data.success && data.users) { + return data.users.find(user => + (user.email === email || user.Email === email) + ); + } + return null; + } catch (error) { + console.error('Error fetching users:', error); + return null; + } +} + // Email shift details to all volunteers async function emailShiftDetails() { if (!currentShiftData) { @@ -589,10 +711,15 @@ try { emailShiftDetails, setupVolunteerModalEventListeners, loadAllUsers, + openVolunteerSMS, + callVolunteer, + getUserByEmail, + updateModalTitle, + updateShiftInList, getCurrentShiftData: () => currentShiftData, getAllUsers: () => allUsers, // Add module info for debugging - moduleVersion: '1.0', + moduleVersion: '1.2', loadedAt: new Date().toISOString() }; diff --git a/map/app/public/js/admin-shifts.js b/map/app/public/js/admin-shifts.js index de99e73..73b8495 100644 --- a/map/app/public/js/admin-shifts.js +++ b/map/app/public/js/admin-shifts.js @@ -59,13 +59,26 @@ function displayAdminShifts(shifts) { console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []); + // Generate list of first names for volunteers + const firstNames = shift.signups ? shift.signups.map(volunteer => { + const fullName = volunteer['User Name'] || volunteer['User Email'] || 'Unknown'; + // Extract first name (everything before first space, or email username if no space) + const firstName = fullName.includes(' ') ? fullName.split(' ')[0] : + fullName.includes('@') ? fullName.split('@')[0] : fullName; + return window.adminCore.escapeHtml(firstName); + }).slice(0, 8) : []; // Limit to first 8 names to avoid overflow + + const namesDisplay = firstNames.length > 0 ? + `(${firstNames.join(', ')}${firstNames.length === 8 && signupCount > 8 ? '...' : ''})` : + ''; + return ` -
+

${window.adminCore.escapeHtml(shift.Title)}

📅 ${shiftDate.toLocaleDateString()} | ⏰ ${shift['Start Time']} - ${shift['End Time']}

📍 ${window.adminCore.escapeHtml(shift.Location || 'TBD')}

-

👥 ${signupCount}/${shift['Max Volunteers']} volunteers

+

👥 ${signupCount}/${shift['Max Volunteers']} volunteers ${namesDisplay}

${shift.Status || 'Open'}

${isPublic ? '🌐 Public' : '🔒 Private'}

${isPublic ? ` diff --git a/map/app/public/js/admin-users.js b/map/app/public/js/admin-users.js index 7d03aac..0ac8fcb 100644 --- a/map/app/public/js/admin-users.js +++ b/map/app/public/js/admin-users.js @@ -72,6 +72,7 @@ function displayUsers(users) { Email Name + Phone Role Created Actions @@ -105,6 +106,7 @@ function displayUsers(users) { ${window.adminCore.escapeHtml(user.email || user.Email || 'N/A')} ${window.adminCore.escapeHtml(user.name || user.Name || 'N/A')} + ${window.adminCore.escapeHtml(user.phone || user.Phone || 'N/A')} ${userType.charAt(0).toUpperCase() + userType.slice(1)} @@ -114,12 +116,31 @@ function displayUsers(users) { ${formattedDate} @@ -222,6 +243,7 @@ async function createUser(e) { const emailInput = document.getElementById('user-email'); const passwordInput = document.getElementById('user-password'); const nameInput = document.getElementById('user-name'); + const phoneInput = document.getElementById('user-phone'); const userTypeSelect = document.getElementById('user-type'); const expireDaysInput = document.getElementById('user-expire-days'); const adminCheckbox = document.getElementById('user-is-admin'); @@ -229,6 +251,7 @@ async function createUser(e) { const email = emailInput?.value.trim(); const password = passwordInput?.value; const name = nameInput?.value.trim(); + const phone = phoneInput?.value.trim(); const userType = userTypeSelect?.value; const expireDays = userType === 'temp' ? parseInt(expireDaysInput?.value) : null; @@ -254,6 +277,7 @@ async function createUser(e) { email, password, name: name || '', + phone: phone || '', isAdmin: userType === 'admin' || admin, userType, expireDays diff --git a/map/build-nocodb.sh b/map/build-nocodb.sh index 4d9873c..276c8fa 100755 --- a/map/build-nocodb.sh +++ b/map/build-nocodb.sh @@ -12,10 +12,19 @@ # 5. shift_signups - Table for tracking signups to shifts with source tracking and phone numbers # 6. cuts - Table for storing polygon overlays for the map # -# Updated: August 2025 - Added public shift support, signup source tracking, phone numbers +# Updated: September 2025 - Added data migration option from existing NocoDB bases +# Usage: +# ./build-nocodb.sh # Create new base only +# ./build-nocodb.sh --migrate-data # Create new base with data migration option +# ./build-nocodb.sh --help # Show usage information set -e # Exit on any error +# Global variables for migration +MIGRATE_DATA=false +SOURCE_BASE_ID="" +SOURCE_TABLE_IDS="" + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -40,6 +49,62 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } +# Function to show usage information +show_usage() { + cat << EOF +NocoDB Auto-Setup Script + +USAGE: + $0 [OPTIONS] + +OPTIONS: + --migrate-data Skip interactive prompt and enable data migration mode + --help Show this help message + +DESCRIPTION: + This script creates a new NocoDB base with the required tables for the Map Viewer application. + + Interactive mode (default): Prompts you to choose between fresh installation or data migration. + + With --migrate-data option, skips the prompt and goes directly to migration setup, allowing + you to select an existing base and migrate data from specific tables to the new base. + +EXAMPLES: + $0 # Interactive mode - choose fresh or migration + $0 --migrate-data # Skip prompt, go directly to migration setup + $0 --help # Show this help + +MIGRATION FEATURES: + - Automatically detects current base from .env file settings + - Interactive base and table selection with clear guidance + - Filters out auto-generated columns (CreatedAt, UpdatedAt, etc.) + - Preserves original data (creates new base, doesn't modify existing) + - Progress tracking during import with detailed success/failure reporting + +EOF +} + +# Parse command line arguments +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + --migrate-data) + MIGRATE_DATA=true + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac + done +} + # Load environment variables if [ -f ".env" ]; then # Use set -a to automatically export variables @@ -58,6 +123,33 @@ if [ -z "$NOCODB_API_URL" ] || [ -z "$NOCODB_API_TOKEN" ]; then exit 1 fi +# Check for required dependencies +check_dependencies() { + local missing_deps=() + + # Check for jq (required for JSON parsing in migration) + if ! command -v jq &> /dev/null; then + missing_deps+=("jq") + fi + + # Check for curl (should be available but let's verify) + if ! command -v curl &> /dev/null; then + missing_deps+=("curl") + fi + + if [[ ${#missing_deps[@]} -gt 0 ]]; then + print_error "Missing required dependencies: ${missing_deps[*]}" + print_error "Please install the missing dependencies before running this script" + print_status "On Ubuntu/Debian: sudo apt-get install ${missing_deps[*]}" + print_status "On CentOS/RHEL: sudo yum install ${missing_deps[*]}" + print_status "On macOS: brew install ${missing_deps[*]}" + exit 1 + fi +} + +# Check dependencies +check_dependencies + # Extract base URL from API URL and set up v2 API endpoints BASE_URL=$(echo "$NOCODB_API_URL" | sed 's|/api/v1||') API_BASE_V1="$NOCODB_API_URL" @@ -205,6 +297,332 @@ test_api_connectivity() { fi } +# Function to list all available bases +list_available_bases() { + print_status "Fetching available NocoDB bases..." + + local response + response=$(make_api_call "GET" "/meta/bases" "" "Fetching bases list" "v2") + + if [[ $? -eq 0 && -n "$response" ]]; then + echo "$response" + return 0 + else + print_error "Failed to fetch bases list" + return 1 + fi +} + +# Function to list tables in a specific base +list_base_tables() { + local base_id=$1 + + print_status "Fetching tables for base: $base_id" + + local response + response=$(make_api_call "GET" "/meta/bases/$base_id/tables" "" "Fetching tables list" "v2") + + if [[ $? -eq 0 && -n "$response" ]]; then + echo "$response" + return 0 + else + print_error "Failed to fetch tables list for base: $base_id" + return 1 + fi +} + +# Function to export data from a table +export_table_data() { + local base_id=$1 + local table_id=$2 + local table_name=$3 + local limit=${4:-1000} # Default limit of 1000 records + + print_status "Exporting data from table: $table_name (ID: $table_id)" + + local response + response=$(make_api_call "GET" "/tables/$table_id/records?limit=$limit" "" "Exporting data from $table_name" "v2") + + if [[ $? -eq 0 && -n "$response" ]]; then + echo "$response" + return 0 + else + print_error "Failed to export data from table: $table_name" + return 1 + fi +} + +# Function to import data into a table +import_table_data() { + local base_id=$1 + local table_id=$2 + local table_name=$3 + local data=$4 + + # Check if data contains records + local record_count=$(echo "$data" | grep -o '"list":\[' | wc -l) + + if [[ $record_count -eq 0 ]]; then + print_warning "No records found in source table: $table_name" + return 0 + fi + + # Extract the records array from the response + local records_array + records_array=$(echo "$data" | jq -r '.list' 2>/dev/null) + + if [[ -z "$records_array" || "$records_array" == "[]" || "$records_array" == "null" ]]; then + print_warning "No records to import for table: $table_name" + return 0 + fi + + print_status "Importing data into table: $table_name (ID: $table_id)" + + # Count total records first + local total_records + total_records=$(echo "$records_array" | jq 'length' 2>/dev/null) + print_status "Found $total_records records to import" + + local import_count=0 + local success_count=0 + + # Create temporary file to track results across subshell + local temp_file="/tmp/nocodb_import_$$" + echo "0" > "$temp_file" + + # Parse records and import them one by one (to handle potential ID conflicts) + echo "$records_array" | jq -c '.[]' 2>/dev/null | while read -r record; do + import_count=$((import_count + 1)) + + # Remove auto-generated and system columns that can cause conflicts + local cleaned_record + cleaned_record=$(echo "$record" | jq ' + del(.Id) | + del(.id) | + del(.ID) | + del(.CreatedAt) | + del(.UpdatedAt) | + del(.created_at) | + del(.updated_at) | + del(.ncRecordId) | + del(.ncRecordHash) + ' 2>/dev/null) + + if [[ -z "$cleaned_record" || "$cleaned_record" == "{}" || "$cleaned_record" == "null" ]]; then + print_warning "Skipping empty record $import_count in $table_name" + continue + fi + + # Use a simpler call without the make_api_call wrapper for better error handling + local response + local http_code + + response=$(curl -s -w "%{http_code}" -X "POST" \ + -H "xc-token: $NOCODB_API_TOKEN" \ + -H "Content-Type: application/json" \ + --max-time 30 \ + -d "$cleaned_record" \ + "$API_BASE_V2/tables/$table_id/records" 2>/dev/null) + + http_code="${response: -3}" + + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + success_count=$(cat "$temp_file") + success_count=$((success_count + 1)) + echo "$success_count" > "$temp_file" + print_status "✓ Imported record $import_count/$total_records" + else + local response_body="${response%???}" + print_warning "✗ Failed to import record $import_count/$total_records: $response_body" + fi + done + + # Read final success count + local final_success_count=$(cat "$temp_file" 2>/dev/null || echo "0") + rm -f "$temp_file" + + print_success "Data import completed for table: $table_name ($final_success_count/$total_records records imported)" +} + +# Function to prompt user for base selection +select_source_base() { + print_status "Fetching available bases for migration..." + + local bases_response + bases_response=$(list_available_bases) + + if [[ $? -ne 0 ]]; then + print_error "Could not fetch available bases" + return 1 + fi + + # Parse and display available bases + local bases_info + bases_info=$(echo "$bases_response" | jq -r '.list[] | "\(.id)|\(.title)|\(.description // "No description")"' 2>/dev/null) + + if [[ -z "$bases_info" ]]; then + print_warning "No existing bases found for migration" + return 1 + fi + + # Try to detect current base from .env file + local current_base_id="" + if [[ -n "$NOCODB_VIEW_URL" ]]; then + current_base_id=$(extract_base_id_from_url "$NOCODB_VIEW_URL") + fi + + echo "" + print_status "Available bases for data migration:" + print_status "=====================================" + + local counter=1 + local suggested_option="" + + echo "$bases_info" | while IFS='|' read -r base_id title description; do + local marker="" + if [[ "$base_id" == "$current_base_id" ]]; then + marker=" ⭐ [CURRENT]" + suggested_option="$counter" + fi + echo " $counter) $title$marker" + echo " ID: $base_id" + echo " Description: $description" + echo "" + counter=$((counter + 1)) + done + + echo "" + if [[ -n "$current_base_id" ]]; then + print_warning "⭐ Detected current base from .env file (marked above)" + echo -n "Enter the number of the base to migrate from (or 'skip'): " + else + echo -n "Enter the number of the base you want to migrate from (or 'skip'): " + fi + + read -r selection + + if [[ "$selection" == "skip" ]]; then + print_status "Skipping data migration" + return 1 + fi + + if ! [[ "$selection" =~ ^[0-9]+$ ]]; then + print_error "Invalid selection. Please enter a number or 'skip'" + return 1 + fi + + # Get the selected base ID + local selected_base_id + selected_base_id=$(echo "$bases_info" | sed -n "${selection}p" | cut -d'|' -f1) + + if [[ -z "$selected_base_id" ]]; then + print_error "Invalid selection" + return 1 + fi + + SOURCE_BASE_ID="$selected_base_id" + print_success "Selected base ID: $SOURCE_BASE_ID" + return 0 +} + +# Function to select tables for migration +select_migration_tables() { + local source_base_id=$1 + + print_status "Fetching tables from source base..." + + local tables_response + tables_response=$(list_base_tables "$source_base_id") + + if [[ $? -ne 0 ]]; then + print_error "Could not fetch tables from source base" + return 1 + fi + + # Parse and display available tables + local tables_info + tables_info=$(echo "$tables_response" | jq -r '.list[] | "\(.id)|\(.title)|\(.table_name)"' 2>/dev/null) + + if [[ -z "$tables_info" ]]; then + print_warning "No tables found in source base" + return 1 + fi + + echo "" + print_status "Available tables in source base:" + print_status "================================" + + local counter=1 + echo "$tables_info" | while IFS='|' read -r table_id title table_name; do + echo " $counter) $title ($table_name)" + echo " Table ID: $table_id" + echo "" + counter=$((counter + 1)) + done + + echo "" + print_status "Select tables to migrate (comma-separated numbers, or 'all' for all tables):" + echo -n "Selection: " + read -r table_selection + + if [[ "$table_selection" == "all" ]]; then + SOURCE_TABLE_IDS=$(echo "$tables_info" | cut -d'|' -f1 | tr '\n' ',' | sed 's/,$//') + else + local selected_ids="" + IFS=',' read -ra selections <<< "$table_selection" + for selection in "${selections[@]}"; do + selection=$(echo "$selection" | xargs) # Trim whitespace + if [[ "$selection" =~ ^[0-9]+$ ]]; then + local table_id + table_id=$(echo "$tables_info" | sed -n "${selection}p" | cut -d'|' -f1) + if [[ -n "$table_id" ]]; then + selected_ids="$selected_ids$table_id," + fi + fi + done + SOURCE_TABLE_IDS=$(echo "$selected_ids" | sed 's/,$//') + fi + + if [[ -z "$SOURCE_TABLE_IDS" ]]; then + print_error "No valid tables selected" + return 1 + fi + + print_success "Selected table IDs: $SOURCE_TABLE_IDS" + return 0 +} + +# Function to migrate data from source to destination +migrate_table_data() { + local source_base_id=$1 + local dest_base_id=$2 + local source_table_id=$3 + local dest_table_id=$4 + local table_name=$5 + + print_status "Migrating data from $table_name..." + + # Export data from source table + local exported_data + exported_data=$(export_table_data "$source_base_id" "$source_table_id" "$table_name") + + if [[ $? -ne 0 ]]; then + print_error "Failed to export data from source table: $table_name" + return 1 + fi + + # Import data to destination table + import_table_data "$dest_base_id" "$dest_table_id" "$table_name" "$exported_data" + + if [[ $? -eq 0 ]]; then + print_success "Successfully migrated data for table: $table_name" + return 0 + else + print_error "Failed to migrate data for table: $table_name" + return 1 + fi +} + # Function to create new project with timestamp create_new_project() { # Generate unique project name with timestamp @@ -410,6 +828,15 @@ create_login_table() { "uidt": "SingleLineText", "rqd": false }, + { + "column_name": "phone", + "title": "Phone", + "uidt": "PhoneNumber", + "rqd": false, + "meta": { + "validate": true + } + }, { "column_name": "admin", "title": "Admin", @@ -1038,11 +1465,86 @@ update_env_file() { fi } +# Function to extract base ID from URL +extract_base_id_from_url() { + local url="$1" + echo "$url" | grep -o '/nc/[^/]*' | sed 's|/nc/||' +} + +# Function to prompt user about data migration +prompt_migration_choice() { + print_status "NocoDB Auto-Setup - Migration Options" + print_status "=====================================" + echo "" + print_status "This script will create a new NocoDB base with fresh tables." + echo "" + print_status "Migration Options:" + print_status " 1) Fresh installation (create new base with default data)" + print_status " 2) Migrate from existing base (preserve your current data)" + echo "" + + # Check if we have existing URLs in .env to suggest migration + if [[ -n "$NOCODB_VIEW_URL" ]]; then + local current_base_id=$(extract_base_id_from_url "$NOCODB_VIEW_URL") + print_warning "Detected existing base in .env: $current_base_id" + print_warning "You may want to migrate data from your current base." + fi + + echo "" + echo -n "Choose option (1 or 2): " + read -r choice + + case $choice in + 1) + print_status "Selected: Fresh installation" + MIGRATE_DATA=false + return 0 + ;; + 2) + print_status "Selected: Data migration" + MIGRATE_DATA=true + return 0 + ;; + *) + print_error "Invalid choice. Please enter 1 or 2." + prompt_migration_choice + ;; + esac +} + # Main execution main() { + # Parse command line arguments + parse_arguments "$@" + print_status "Starting NocoDB Auto-Setup..." print_status "================================" + # Always prompt for migration choice unless --migrate-data was explicitly passed + if [[ "$MIGRATE_DATA" != "true" ]]; then + prompt_migration_choice + fi + + # Handle data migration setup if requested + if [[ "$MIGRATE_DATA" == "true" ]]; then + print_status "" + print_status "=== Data Migration Setup ===" + + if select_source_base; then + if select_migration_tables "$SOURCE_BASE_ID"; then + print_success "Migration setup completed" + print_warning "Data will be migrated after creating the new base and tables" + else + print_warning "Table selection failed, proceeding without migration" + MIGRATE_DATA=false + fi + else + print_warning "Base selection failed, proceeding without migration" + MIGRATE_DATA=false + fi + print_status "" + fi + # Always create a new project print_status "Creating new base..." print_warning "This script creates a NEW base and does NOT modify existing data" @@ -1079,14 +1581,53 @@ main() { # Wait a moment for tables to be fully created sleep 3 - # Create default data - print_status "Setting up default data..." - - # Create default admin user - create_default_admin "$BASE_ID" "$LOGIN_TABLE_ID" - - # Create default settings row (includes both start location and walk sheet config) - create_default_start_location "$BASE_ID" "$SETTINGS_TABLE_ID" + # Handle data migration if enabled + if [[ "$MIGRATE_DATA" == "true" && -n "$SOURCE_BASE_ID" && -n "$SOURCE_TABLE_IDS" ]]; then + print_status "================================" + print_status "Starting data migration..." + print_status "================================" + + # Create mapping of table names to new table IDs + declare -A new_table_map=( + ["locations"]="$LOCATIONS_TABLE_ID" + ["login"]="$LOGIN_TABLE_ID" + ["settings"]="$SETTINGS_TABLE_ID" + ["shifts"]="$SHIFTS_TABLE_ID" + ["shift_signups"]="$SHIFT_SIGNUPS_TABLE_ID" + ["cuts"]="$CUTS_TABLE_ID" + ) + + # Get source table information + local source_tables_response + source_tables_response=$(list_base_tables "$SOURCE_BASE_ID") + + # Migrate each selected table + IFS=',' read -ra table_ids <<< "$SOURCE_TABLE_IDS" + for source_table_id in "${table_ids[@]}"; do + # Get table name from source + local table_info + table_info=$(echo "$source_tables_response" | jq -r ".list[] | select(.id == \"$source_table_id\") | .table_name" 2>/dev/null) + + if [[ -n "$table_info" && -n "${new_table_map[$table_info]}" ]]; then + migrate_table_data "$SOURCE_BASE_ID" "$BASE_ID" "$source_table_id" "${new_table_map[$table_info]}" "$table_info" + else + print_warning "Skipping migration for unknown table: $table_info (ID: $source_table_id)" + fi + done + + print_status "================================" + print_success "Data migration completed!" + print_status "================================" + else + # Create default data only if not migrating + print_status "Setting up default data..." + + # Create default admin user + create_default_admin "$BASE_ID" "$LOGIN_TABLE_ID" + + # Create default settings row (includes both start location and walk sheet config) + create_default_start_location "$BASE_ID" "$SETTINGS_TABLE_ID" + fi # Update .env file with new table URLs update_env_file "$BASE_ID" "$LOCATIONS_TABLE_ID" "$LOGIN_TABLE_ID" "$SETTINGS_TABLE_ID" "$SHIFTS_TABLE_ID" "$SHIFT_SIGNUPS_TABLE_ID" "$CUTS_TABLE_ID" @@ -1100,15 +1641,30 @@ main() { print_status "Next steps:" print_status "1. Login to your NocoDB instance at: $BASE_URL" print_status "2. Your .env file has been automatically updated with the new table URLs!" - print_status "3. The default admin user is: admin@thebunkerops.ca with password: admin123" - print_status "4. IMPORTANT: Change the default password after first login!" - print_status "5. Start adding your location data!" + + if [[ "$MIGRATE_DATA" == "true" && -n "$SOURCE_BASE_ID" && -n "$SOURCE_TABLE_IDS" ]]; then + print_status "3. Your existing data has been migrated to the new base!" + print_status "4. Review the migrated data and verify everything transferred correctly" + print_status "5. If you had custom admin users, you may need to update passwords" + else + print_status "3. The default admin user is: admin@thebunkerops.ca with password: admin123" + print_status "4. IMPORTANT: Change the default password after first login!" + print_status "5. Start adding your location data!" + fi print_warning "" print_warning "IMPORTANT: This script created a NEW base. Your existing data was NOT modified." print_warning "Your .env file has been automatically updated with the new table URLs." print_warning "A backup of your previous .env file was created with a timestamp." - print_warning "SECURITY: Change the default admin password immediately after first login!" + + if [[ "$MIGRATE_DATA" != "true" ]]; then + print_warning "SECURITY: Change the default admin password immediately after first login!" + fi + + if [[ "$MIGRATE_DATA" == "true" ]]; then + print_warning "DATA MIGRATION: Verify all migrated data is correct before using in production!" + print_warning "The original base remains unchanged as a backup." + fi } # Check if script is being run directly diff --git a/map/files-explainer.md b/map/files-explainer.md index f62c4e7..d923c66 100644 --- a/map/files-explainer.md +++ b/map/files-explainer.md @@ -24,7 +24,14 @@ Documents the development and requirements of the NocoDB automation script for t # build-nocodb.sh -Bash script to automate creation of required NocoDB tables and default data for the app. +Bash script to automate creation of required NocoDB tables and default data for the app. Features: +- Creates fresh NocoDB base with 6 required tables (locations, login, settings, shifts, shift_signups, cuts) +- Optional data migration from existing NocoDB bases (--migrate-data flag) +- Interactive base and table selection for migration +- Preserves original data while creating new base +- Auto-updates .env file with new table URLs +- Dependency checking (requires jq and curl) +- Comprehensive error handling and user feedback # combined.log diff --git a/map/test_print_debug.html b/map/test_print_debug.html deleted file mode 100644 index 15a6361..0000000 --- a/map/test_print_debug.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - Print Debug Test - - - -

Print Debug Test

-

Check the browser console for test results.

-

This tests the enhanced CutPrintUtils functionality.

- -

Key Improvements Made:

-
    -
  • ✅ Auto-load locations when printing if not already loaded
  • -
  • ✅ Auto-display locations on map for print capture
  • -
  • ✅ Enhanced map capture with html2canvas (priority #1)
  • -
  • ✅ Improved dom-to-image capture with better filtering
  • -
  • ✅ Better UI state management (toggle button updates)
  • -
  • ✅ Enhanced debugging and logging
  • -
  • ✅ Auto-show locations when viewing cuts (if enabled)
  • -
- -

Root Cause Analysis:

-

The issue was that locations were not automatically displayed on the map when viewing a cut or printing. - The print function expected locations to be visible but they were only shown when the user manually clicked "Show Locations".

- -

Solution:

-
    -
  1. Print Enhancement: The print function now ensures locations are loaded and displayed before capturing the map
  2. -
  3. View Enhancement: When viewing a cut, locations are automatically loaded if the cut has show_locations enabled
  4. -
  5. Capture Enhancement: Improved map capture methods with html2canvas as primary method
  6. -
  7. State Management: Better synchronization between location visibility and UI state
  8. -
- -