diff --git a/map/app/public/admin.html b/map/app/public/admin.html index 1d66e86..8909479 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -6,6 +6,10 @@ Admin Panel - NocoDB Map Viewer + + + + - - + + diff --git a/map/app/public/css/admin.css b/map/app/public/css/admin.css index 57e734b..3c5e053 100644 --- a/map/app/public/css/admin.css +++ b/map/app/public/css/admin.css @@ -307,10 +307,10 @@ aspect-ratio: 8.5 / 11; background: white; box-shadow: 0 2px 10px rgba(0,0,0,0.1); - padding: 20px; + padding: 40px; /* Match print padding */ margin: 0 auto; overflow: auto; - font-size: 10px; + font-size: 12px; /* Match print font size */ line-height: 1.4; position: relative; } @@ -326,17 +326,38 @@ } .walk-sheet-page { - position: absolute; + position: fixed; left: 0; top: 0; width: 8.5in; height: 11in; max-width: none; - padding: 0.5in; + padding: 0.5in; /* 0.5 inch margins */ margin: 0; box-shadow: none; font-size: 12pt; + page-break-after: always; } + + /* Ensure QR codes print properly */ + .ws-qr-code img { + width: 100px !important; + height: 100px !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} + +/* Adjust preview scaling */ +.walk-sheet-preview .walk-sheet-page { + transform: scale(0.5); + transform-origin: top left; + margin-bottom: -50%; /* Compensate for scale */ +} + +.walk-sheet-preview { + overflow: hidden; + height: 550px; /* Fixed height for preview container */ } /* Walk Sheet Content Styles */ @@ -539,3 +560,24 @@ --transition: all 0.2s ease; --header-height: 60px; } + +/* Crosshair styling */ +.crosshair { + pointer-events: none; + z-index: 1000; +} + +.crosshair div { + border-radius: 1px; + opacity: 0.8; +} + +/* Admin map styling */ +.admin-map { + position: relative; + cursor: crosshair; +} + +.admin-map .leaflet-container { + cursor: crosshair; +} diff --git a/map/app/public/favicon.ico b/map/app/public/favicon.ico new file mode 100644 index 0000000..09bc74b --- /dev/null +++ b/map/app/public/favicon.ico @@ -0,0 +1 @@ + diff --git a/map/app/public/js/admin.js b/map/app/public/js/admin.js index 230c6a3..2a0da69 100644 --- a/map/app/public/js/admin.js +++ b/map/app/public/js/admin.js @@ -48,6 +48,24 @@ function initializeAdminMap() { minZoom: 2 }).addTo(adminMap); + // Add crosshair to center of map + const crosshairIcon = L.divIcon({ + className: 'crosshair', + iconSize: [20, 20], + html: '
' + }); + + const crosshair = L.marker(adminMap.getCenter(), { + icon: crosshairIcon, + interactive: false, + zIndexOffset: 1000 + }).addTo(adminMap); + + // Update crosshair position when map moves + adminMap.on('move', function() { + crosshair.setLatLng(adminMap.getCenter()); + }); + // Add click handler to set location adminMap.on('click', handleMapClick); @@ -128,31 +146,46 @@ function updateCoordinatesFromMap() { // Setup event listeners function setupEventListeners() { // Use current view button - document.getElementById('use-current-view').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'); - }); + 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'); + }); + } // Save button - document.getElementById('save-start-location').addEventListener('click', saveStartLocation); + const saveLocationBtn = document.getElementById('save-start-location'); + if (saveLocationBtn) { + saveLocationBtn.addEventListener('click', saveStartLocation); + } // Coordinate input changes - document.getElementById('start-lat').addEventListener('change', updateMapFromInputs); - document.getElementById('start-lng').addEventListener('change', updateMapFromInputs); - document.getElementById('start-zoom').addEventListener('change', updateMapFromInputs); + 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 - document.getElementById('save-walk-sheet').addEventListener('click', saveWalkSheetConfig); - document.getElementById('preview-walk-sheet').addEventListener('click', generateWalkSheetPreview); - document.getElementById('print-walk-sheet').addEventListener('click', printWalkSheet); - document.getElementById('refresh-preview').addEventListener('click', generateWalkSheetPreview); + 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( @@ -161,24 +194,65 @@ function setupEventListeners() { ); walkSheetInputs.forEach(input => { - input.addEventListener('input', debounce(() => { - generateWalkSheetPreview(); - }, 500)); + if (input) { + input.addEventListener('input', debounce(() => { + generateWalkSheetPreview(); + }, 500)); + } }); // 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`); - let previousUrl = urlInput.value; - - urlInput.addEventListener('change', () => { - if (urlInput.value !== previousUrl) { - // URL changed, clear stored QR code - delete storedQRCodes[i]; - previousUrl = urlInput.value; + if (urlInput) { + let previousUrl = urlInput.value; + + urlInput.addEventListener('change', () => { + if (urlInput.value !== previousUrl) { + // URL changed, clear stored QR code + delete storedQRCodes[i]; + previousUrl = urlInput.value; + } + }); + } + } +} + +// Setup navigation between admin sections +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 walk sheet, generate preview + if (targetId === 'walk-sheet') { + generateWalkSheetPreview(); } }); - } + }); } // Update map from input fields @@ -245,19 +319,24 @@ async function saveStartLocation() { // Save walk sheet configuration async function saveWalkSheetConfig() { const config = { - walk_sheet_title: document.getElementById('walk-sheet-title').value, - walk_sheet_subtitle: document.getElementById('walk-sheet-subtitle').value, - walk_sheet_footer: document.getElementById('walk-sheet-footer').value, - qr_code_1_url: document.getElementById('qr-code-1-url').value, - qr_code_1_label: document.getElementById('qr-code-1-label').value, - qr_code_2_url: document.getElementById('qr-code-2-url').value, - qr_code_2_label: document.getElementById('qr-code-2-label').value, - qr_code_3_url: document.getElementById('qr-code-3-url').value, - qr_code_3_label: document.getElementById('qr-code-3-label').value + walk_sheet_title: document.getElementById('walk-sheet-title')?.value || '', + walk_sheet_subtitle: document.getElementById('walk-sheet-subtitle')?.value || '', + walk_sheet_footer: document.getElementById('walk-sheet-footer')?.value || '', + qr_code_1_url: document.getElementById('qr-code-1-url')?.value || '', + qr_code_1_label: document.getElementById('qr-code-1-label')?.value || '', + qr_code_2_url: document.getElementById('qr-code-2-url')?.value || '', + qr_code_2_label: document.getElementById('qr-code-2-label')?.value || '', + qr_code_3_url: document.getElementById('qr-code-3-url')?.value || '', + qr_code_3_label: document.getElementById('qr-code-3-label')?.value || '' }; // Show loading state const saveButton = document.getElementById('save-walk-sheet'); + if (!saveButton) { + showStatus('Save button not found', 'error'); + return; + } + const originalText = saveButton.textContent; saveButton.textContent = 'Saving...'; saveButton.disabled = true; @@ -280,7 +359,7 @@ async function saveWalkSheetConfig() { if (data.qrCodes) { for (let i = 1; i <= 3; i++) { if (data.qrCodes[`qr_code_${i}_image`]) { - storedQRCodes[i] = data.qrCodes[`qr_code_${i}_image`]; + storedQRCodes[`qr_code_${i}_image`] = data.qrCodes[`qr_code_${i}_image`]; } } } @@ -302,9 +381,9 @@ async function saveWalkSheetConfig() { // Generate walk sheet preview function generateWalkSheetPreview() { - const title = document.getElementById('walk-sheet-title').value || 'Campaign Walk Sheet'; - const subtitle = document.getElementById('walk-sheet-subtitle').value || 'Door-to-Door Canvassing Form'; - const footer = document.getElementById('walk-sheet-footer').value || 'Thank you for your support!'; + const title = document.getElementById('walk-sheet-title')?.value || 'Campaign Walk Sheet'; + const subtitle = document.getElementById('walk-sheet-subtitle')?.value || 'Door-to-Door Canvassing Form'; + const footer = document.getElementById('walk-sheet-footer')?.value || 'Thank you for your support!'; let previewHTML = `
@@ -316,30 +395,21 @@ function generateWalkSheetPreview() { // Add QR codes section const qrCodesHTML = []; for (let i = 1; i <= 3; i++) { - const url = document.getElementById(`qr-code-${i}-url`).value; - const label = document.getElementById(`qr-code-${i}-label`).value; + const urlInput = document.getElementById(`qr-code-${i}-url`); + const labelInput = document.getElementById(`qr-code-${i}-label`); + + const url = urlInput?.value || ''; + const label = labelInput?.value || ''; if (url) { - // Check if we have a stored QR code image - if (storedQRCodes[i] && storedQRCodes[i].url) { - // Use stored QR code image - qrCodesHTML.push(` -
-
- QR Code ${i} -
-
${escapeHtml(label) || `QR Code ${i}`}
+ qrCodesHTML.push(` +
+
+
- `); - } else { - // Generate QR code client-side as fallback - qrCodesHTML.push(` -
-
-
${escapeHtml(label) || `QR Code ${i}`}
-
- `); - } +
${escapeHtml(label || `QR Code ${i}`)}
+
+ `); } } @@ -393,14 +463,17 @@ function generateWalkSheetPreview() {
- -
+ +
+ ☐ Yes + ☐ No +
- +
@@ -416,97 +489,149 @@ function generateWalkSheetPreview() {
`; - // Update preview - const previewContent = document.getElementById('walk-sheet-preview-content'); - previewContent.innerHTML = previewHTML; - - // Add footer (positioned absolutely in CSS) + // Add footer if (footer) { - const footerDiv = document.createElement('div'); - footerDiv.className = 'ws-footer'; - footerDiv.innerHTML = escapeHtml(footer); - previewContent.appendChild(footerDiv); + previewHTML += ` + + `; } - // Generate client-side QR codes for items without stored images - setTimeout(() => { - for (let i = 1; i <= 3; i++) { - const url = document.getElementById(`qr-code-${i}-url`).value; - if (url && !storedQRCodes[i]) { - const qrContainer = document.getElementById(`preview-qr-${i}`); - if (qrContainer && typeof QRCode !== 'undefined') { - qrContainer.innerHTML = ''; - new QRCode(qrContainer, { - text: url, - width: 80, - height: 80, - correctLevel: QRCode.CorrectLevel.M - }); - } + // Update preview + const previewContent = document.getElementById('walk-sheet-preview-content'); + if (previewContent) { + previewContent.innerHTML = previewHTML; + + // Generate QR codes after DOM is updated + setTimeout(() => { + generatePreviewQRCodes(); + }, 100); + } else { + console.warn('Walk sheet preview content container not found'); + } +} + +// Generate QR codes for preview +async function generatePreviewQRCodes() { + for (let i = 1; i <= 3; i++) { + const urlInput = document.getElementById(`qr-code-${i}-url`); + const url = urlInput?.value || ''; + const qrContainer = document.getElementById(`preview-qr-${i}`); + + if (url && qrContainer) { + try { + // Use our local QR code generation endpoint + const qrImageUrl = `/api/qr?text=${encodeURIComponent(url)}&size=100`; + qrContainer.innerHTML = `QR Code ${i}`; + } catch (error) { + console.error(`Failed to display QR code ${i}:`, error); + qrContainer.innerHTML = '
QR Error
'; } + } else if (qrContainer) { + // Clear empty QR containers + qrContainer.innerHTML = ''; } - }, 100); + } } // Print walk sheet function printWalkSheet() { - // First generate fresh preview + // First generate fresh preview to ensure QR codes are generated generateWalkSheetPreview(); - // Wait for QR codes to generate + // Wait for QR codes to generate, then print setTimeout(() => { - window.print(); + // Create a print-specific window + const printContent = document.getElementById('walk-sheet-preview-content').innerHTML; + const printWindow = window.open('', '_blank'); + + printWindow.document.write(` + + + + Walk Sheet - Print + + + + + +
+ ${printContent} +
+ + + `); + + printWindow.document.close(); + + // Wait for images to load + printWindow.onload = function() { + setTimeout(() => { + printWindow.print(); + printWindow.close(); + }, 250); + }; }, 500); } -// Setup navigation between sections -function setupNavigation() { - const navLinks = document.querySelectorAll('.admin-nav a'); - const sections = document.querySelectorAll('.admin-section'); - - navLinks.forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const targetId = link.getAttribute('href').substring(1); - - // Update active states - navLinks.forEach(l => l.classList.remove('active')); - link.classList.add('active'); - - // Show/hide sections - sections.forEach(section => { - section.style.display = section.id === targetId ? 'block' : 'none'; - }); - }); - }); -} - // Load walk sheet configuration async function loadWalkSheetConfig() { try { const response = await fetch('/api/admin/walk-sheet-config'); const data = await response.json(); - if (data.success && data.config) { + if (data.success && data.data) { // Populate form fields - document.getElementById('walk-sheet-title').value = data.config.walk_sheet_title || ''; - document.getElementById('walk-sheet-subtitle').value = data.config.walk_sheet_subtitle || ''; - document.getElementById('walk-sheet-footer').value = data.config.walk_sheet_footer || ''; + const titleInput = document.getElementById('walk-sheet-title'); + const subtitleInput = document.getElementById('walk-sheet-subtitle'); + const footerInput = document.getElementById('walk-sheet-footer'); - // QR codes + if (titleInput) titleInput.value = data.data.walk_sheet_title || ''; + if (subtitleInput) subtitleInput.value = data.data.walk_sheet_subtitle || ''; + if (footerInput) footerInput.value = data.data.walk_sheet_footer || ''; + + // Store QR code images if they exist for (let i = 1; i <= 3; i++) { - document.getElementById(`qr-code-${i}-url`).value = data.config[`qr_code_${i}_url`] || ''; - document.getElementById(`qr-code-${i}-label`).value = data.config[`qr_code_${i}_label`] || ''; + const urlField = document.getElementById(`qr-code-${i}-url`); + const labelField = document.getElementById(`qr-code-${i}-label`); - // Store QR code image data if available - if (data.config[`qr_code_${i}_image`]) { - storedQRCodes[i] = data.config[`qr_code_${i}_image`]; + if (urlField && data.data[`qr_code_${i}_url`]) { + urlField.value = data.data[`qr_code_${i}_url`]; + } + if (labelField && data.data[`qr_code_${i}_label`]) { + labelField.value = data.data[`qr_code_${i}_label`]; + } + + // Store the QR code image URL if it exists + if (data.data[`qr_code_${i}_image`]) { + storedQRCodes[`qr_code_${i}_image`] = data.data[`qr_code_${i}_image`]; } } // Generate preview generateWalkSheetPreview(); } + } catch (error) { console.error('Failed to load walk sheet config:', error); } diff --git a/map/app/server.js b/map/app/server.js index 12630d5..d4af1ab 100644 --- a/map/app/server.js +++ b/map/app/server.js @@ -200,7 +200,7 @@ app.use(helmet({ directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"], - scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.jsdelivr.net"], imgSrc: ["'self'", "data:", "https://*.tile.openstreetmap.org", "https://unpkg.com"], connectSrc: ["'self'"] } @@ -500,7 +500,18 @@ app.post('/api/admin/start-location', requireAdmin, async (req, res) => { if (existingSettings.length > 0) { // Update existing setting - const settingId = existingSettings[0].id || existingSettings[0].Id; + const setting = existingSettings[0]; + let settingId = setting.id || setting.Id || setting.ID; + + // If we still can't find an ID, log the object structure + if (!settingId) { + logger.error('Cannot find primary key in setting object:', { + setting: setting, + keys: Object.keys(setting) + }); + throw new Error('Unable to find primary key for existing setting'); + } + const updateUrl = `${getUrl}/${settingId}`; // Only include fields that exist in the table @@ -690,22 +701,23 @@ app.get('/api/admin/walk-sheet-config', requireAdmin, async (req, res) => { // Get all settings const response = await axios.get( - `${process.env.NOCODB_API_URL}/api/v1/base/${process.env.NOCODB_PROJECT_ID}/tables/${SETTINGS_SHEET_ID}/rows`, + `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, { headers: { - 'xc-auth': process.env.NOCODB_API_TOKEN + 'xc-token': process.env.NOCODB_API_TOKEN } } ); - if (!response.data?.list || response.data.list.length === 0) { + logger.info('GET Settings response structure:', JSON.stringify(response.data, null, 2)); + if (!response.data?.list || response.data.list.length === 0) { return res.json({ success: true, config: null, source: 'defaults' }); } - + // Find walk sheet settings const walkSheetSettings = {}; const settingKeys = [ @@ -714,7 +726,7 @@ app.get('/api/admin/walk-sheet-config', requireAdmin, async (req, res) => { 'qr_code_2_url', 'qr_code_2_label', 'qr_code_2_image', 'qr_code_3_url', 'qr_code_3_label', 'qr_code_3_image' ]; - + for (const setting of response.data.list) { if (settingKeys.includes(setting.key)) { if (setting.key.includes('_image') && setting.value) { @@ -745,121 +757,86 @@ app.get('/api/admin/walk-sheet-config', requireAdmin, async (req, res) => { } }); -// Save walk sheet configuration +// Save walk sheet configuration (simplified) app.post('/api/admin/walk-sheet-config', requireAdmin, async (req, res) => { try { if (!SETTINGS_SHEET_ID) { + logger.error('SETTINGS_SHEET_ID not configured'); return res.status(400).json({ success: false, error: 'Settings sheet not configured' }); } + logger.info('Using SETTINGS_SHEET_ID:', SETTINGS_SHEET_ID); + const config = req.body; + logger.info('Received config:', JSON.stringify(config, null, 2)); const userEmail = req.session.userEmail; const timestamp = new Date().toISOString(); - // NocoDB configuration - const nocodbConfig = { - apiUrl: process.env.NOCODB_API_URL, - apiToken: process.env.NOCODB_API_TOKEN, - projectId: process.env.NOCODB_PROJECT_ID, - tableId: SETTINGS_SHEET_ID - }; - // Get existing settings const getResponse = await axios.get( - `${process.env.NOCODB_API_URL}/api/v1/base/${process.env.NOCODB_PROJECT_ID}/tables/${SETTINGS_SHEET_ID}/rows`, + `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, { headers: { - 'xc-auth': process.env.NOCODB_API_TOKEN + 'xc-token': process.env.NOCODB_API_TOKEN } } ); + logger.info('Settings response structure:', JSON.stringify(getResponse.data, null, 2)); + const existingSettings = getResponse.data?.list || []; - // Process QR codes - const qrCodeUploads = {}; - for (let i = 1; i <= 3; i++) { - const url = config[`qr_code_${i}_url`]; - const label = config[`qr_code_${i}_label`] || `QR Code ${i}`; - - if (url) { - try { - // Check if URL has changed - const existingUrlSetting = existingSettings.find(s => s.key === `qr_code_${i}_url`); - const urlChanged = !existingUrlSetting || existingUrlSetting.value !== url; - - if (urlChanged) { - // Generate and upload new QR code - const uploadResult = await generateAndUploadQRCode(url, label, nocodbConfig); - if (uploadResult) { - qrCodeUploads[`qr_code_${i}_image`] = uploadResult; - - // Delete old QR code if exists - const existingImageSetting = existingSettings.find(s => s.key === `qr_code_${i}_image`); - if (existingImageSetting?.value) { - await deleteQRCodeFromNocoDB(existingImageSetting.value, nocodbConfig); - } - } - } - } catch (error) { - logger.error(`Failed to process QR code ${i}:`, error); - } - } else { - // If URL is empty, delete existing QR code - const existingImageSetting = existingSettings.find(s => s.key === `qr_code_${i}_image`); - if (existingImageSetting?.value) { - await deleteQRCodeFromNocoDB(existingImageSetting.value, nocodbConfig); - } - qrCodeUploads[`qr_code_${i}_image`] = null; - } - } + // Simple approach: Just save the text configuration (no QR code uploads for now) + const simpleSettings = { + walk_sheet_title: config.walk_sheet_title || '', + walk_sheet_subtitle: config.walk_sheet_subtitle || '', + walk_sheet_footer: config.walk_sheet_footer || '', + qr_code_1_url: config.qr_code_1_url || '', + qr_code_1_label: config.qr_code_1_label || '', + qr_code_2_url: config.qr_code_2_url || '', + qr_code_2_label: config.qr_code_2_label || '', + qr_code_3_url: config.qr_code_3_url || '', + qr_code_3_label: config.qr_code_3_label || '' + }; // Update or create each setting - const allSettings = { ...config, ...qrCodeUploads }; - - for (const [key, value] of Object.entries(allSettings)) { + for (const [key, value] of Object.entries(simpleSettings)) { const existingSetting = existingSettings.find(s => s.key === key); let settingData = { key: key, - title: typeof value === 'string' ? value : '', + title: value, + value: value, category: 'walk_sheet_setting', updated_by: userEmail, updated_at: timestamp }; - // Handle different value types - if (key.includes('_image') && value) { - // For image attachments - settingData.value = JSON.stringify(value); - settingData[key] = value; // Also set the attachment field directly - } else { - settingData.value = value || ''; - } - if (existingSetting) { - // Update existing - await axios.put( - `${process.env.NOCODB_API_URL}/api/v1/base/${process.env.NOCODB_PROJECT_ID}/tables/${SETTINGS_SHEET_ID}/rows/${existingSetting.Id}`, + // Update existing - use ID from debug output + logger.info(`Updating setting ${key} with ID ${existingSetting.ID}`); + await axios.patch( + `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}/${existingSetting.ID}`, settingData, { headers: { - 'xc-auth': process.env.NOCODB_API_TOKEN, + 'xc-token': process.env.NOCODB_API_TOKEN, 'Content-Type': 'application/json' } } ); } else { // Create new + logger.info(`Creating new setting ${key}`); await axios.post( - `${process.env.NOCODB_API_URL}/api/v1/base/${process.env.NOCODB_PROJECT_ID}/tables/${SETTINGS_SHEET_ID}/rows`, + `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, settingData, { headers: { - 'xc-auth': process.env.NOCODB_API_TOKEN, + 'xc-token': process.env.NOCODB_API_TOKEN, 'Content-Type': 'application/json' } } @@ -870,19 +847,17 @@ app.post('/api/admin/walk-sheet-config', requireAdmin, async (req, res) => { res.json({ success: true, message: 'Walk sheet configuration saved successfully', - qrCodes: Object.keys(qrCodeUploads).reduce((acc, key) => { - if (qrCodeUploads[key]) { - acc[key] = qrCodeUploads[key]; - } - return acc; - }, {}) + savedSettings: simpleSettings }); } catch (error) { logger.error('Failed to save walk sheet config:', error); + logger.error('Error response:', error.response?.data); + logger.error('Error config:', error.config?.url); res.status(500).json({ success: false, - error: 'Failed to save walk sheet configuration' + error: 'Failed to save walk sheet configuration. No worries; just hit print, and you can save it there too!', + details: error.response?.data || error.message }); } }); @@ -1234,48 +1209,292 @@ app.delete('/api/locations/:id', strictLimiter, async (req, res) => { // Debug endpoint to check settings table structure app.get('/api/debug/settings-table', requireAdmin, async (req, res) => { try { + logger.info('Debug: SETTINGS_SHEET_ID =', SETTINGS_SHEET_ID); + logger.info('Debug: NOCODB_API_URL =', process.env.NOCODB_API_URL); + logger.info('Debug: NOCODB_PROJECT_ID =', process.env.NOCODB_PROJECT_ID); + if (!SETTINGS_SHEET_ID) { return res.json({ - error: 'Settings sheet not configured', + success: false, + error: 'SETTINGS_SHEET_ID not configured', + settingsSheetId: SETTINGS_SHEET_ID, + originalSetting: process.env.NOCODB_SETTINGS_SHEET + }); + } + + // Try the working endpoint + const workingEndpoint = `/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`; + + try { + const response = await axios.get( + `${process.env.NOCODB_API_URL}${workingEndpoint}`, + { + headers: { + 'xc-token': process.env.NOCODB_API_TOKEN + }, + params: { + limit: 5 + } + } + ); + + const records = response.data.list || []; + const sampleRecord = records.length > 0 ? records[0] : null; + + res.json({ + success: true, + settingsSheetId: SETTINGS_SHEET_ID, + workingEndpoint: workingEndpoint, + recordCount: response.data.pageInfo?.totalRows || 0, + sampleRecord: sampleRecord, + availableFields: sampleRecord ? Object.keys(sampleRecord) : [], + allRecords: records + }); + + } catch (error) { + res.json({ + success: false, + error: error.message, + responseData: error.response?.data, + status: error.response?.status, settingsSheetId: SETTINGS_SHEET_ID }); } - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`; - - // Try to get the table structure by fetching all records - const response = await axios.get(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - limit: 1 - } + } catch (error) { + logger.error('Debug settings table error:', error); + res.status(500).json({ + success: false, + error: error.message, + settingsSheetId: SETTINGS_SHEET_ID }); + } +}); + +// Simple QR code test endpoint +app.get('/api/debug/test-qr', requireAdmin, async (req, res) => { + try { + const { generateAndUploadQRCode } = require('./services/qrcode'); - const records = response.data.list || []; - const sampleRecord = records.length > 0 ? records[0] : null; + // Test configuration + const testConfig = { + apiUrl: process.env.NOCODB_API_URL, + apiToken: process.env.NOCODB_API_TOKEN, + projectId: process.env.NOCODB_PROJECT_ID, + tableId: SETTINGS_SHEET_ID + }; + + // Test QR code generation + const testUrl = 'https://example.com/test'; + const testLabel = 'Test QR Code'; + + logger.info('Testing QR code generation...'); + + const result = await generateAndUploadQRCode(testUrl, testLabel, testConfig); res.json({ success: true, - settingsSheetId: SETTINGS_SHEET_ID, - tableUrl: url, - recordCount: response.data.pageInfo?.totalRows || 0, - sampleRecord: sampleRecord, - availableFields: sampleRecord ? Object.keys(sampleRecord) : [] + message: 'QR code generated successfully', + result: result, + testUrl: testUrl, + testLabel: testLabel }); } catch (error) { - logger.error('Error checking settings table:', error); - res.json({ + logger.error('QR code test failed:', error); + res.status(500).json({ + success: false, error: error.message, - response: error.response?.data, - status: error.response?.status + details: error.response?.data || 'No response data' }); } }); +// Local QR code generation endpoint +app.get('/api/qr', async (req, res) => { + try { + const { text, size = 200 } = req.query; + + if (!text) { + return res.status(400).json({ + success: false, + error: 'Text parameter is required' + }); + } + + const { generateQRCode } = require('./services/qrcode'); + + const qrOptions = { + type: 'png', + width: parseInt(size), + margin: 1, + color: { + dark: '#000000', + light: '#FFFFFF' + }, + errorCorrectionLevel: 'M' + }; + + const buffer = await generateQRCode(text, qrOptions); + + res.set({ + 'Content-Type': 'image/png', + 'Content-Length': buffer.length, + 'Cache-Control': 'public, max-age=3600' // Cache for 1 hour + }); + + res.send(buffer); + + } catch (error) { + logger.error('QR code generation error:', error); + res.status(500).json({ + success: false, + error: 'Failed to generate QR code' + }); + } +}); + +// Simple QR test page +app.get('/test-qr', (req, res) => { + res.send(` + + + + QR Code Test + + + +

QR Code Generation Test

+ +
+

Test 1: Direct API Call

+

Try accessing these URLs directly:

+ +
+ +
+

Test 2: Dynamic Generation

+ + +
+
+ +
+

Test 3: Using QRCode Library (like admin panel)

+ + +
+
+ + + + + `); +}); + // Error handling middleware app.use((err, req, res, next) => { logger.error('Unhandled error:', err); diff --git a/map/app/services/qrcode.js b/map/app/services/qrcode.js index 3bece4f..025efed 100644 --- a/map/app/services/qrcode.js +++ b/map/app/services/qrcode.js @@ -61,8 +61,14 @@ async function uploadQRCodeToNocoDB(buffer, filename, config) { }); try { + // Use the base URL without /api/v1 for v2 endpoints + const baseUrl = config.apiUrl.replace('/api/v1', ''); + const uploadUrl = `${baseUrl}/api/v2/storage/upload`; + + logger.info(`Uploading QR code to: ${uploadUrl}`); + const response = await axios({ - url: `${config.apiUrl}/api/v2/storage/upload`, + url: uploadUrl, method: 'post', data: formData, headers: { @@ -74,9 +80,10 @@ async function uploadQRCodeToNocoDB(buffer, filename, config) { } }); + logger.info('QR code upload successful:', response.data); return response.data; } catch (error) { - logger.error('Failed to upload QR code to NocoDB:', error); + logger.error('Failed to upload QR code to NocoDB:', error.response?.data || error.message); throw new Error('Failed to upload QR code'); } }