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 @@
@@ -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 = `

`;
+ } 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');
}
}