const nocodbService = require('../services/nocodb'); const logger = require('../utils/logger'); const config = require('../config'); const { validateUrl, extractId, extractWalkSheetConfig } = require('../utils/helpers'); class SettingsController { // Default settings values static defaultSettings = { walk_sheet_title: 'Campaign Walk Sheet', walk_sheet_subtitle: 'Door-to-Door Canvassing Form', walk_sheet_footer: 'Thank you for your support!', qr_code_1_url: '', qr_code_1_label: '', qr_code_2_url: '', qr_code_2_label: '', qr_code_3_url: '', qr_code_3_label: '' }; async getStartLocation(req, res) { try { const settings = await nocodbService.getLatestSettings(); if (settings) { let lat, lng, zoom; if (settings['Geo-Location']) { const parts = settings['Geo-Location'].split(';'); if (parts.length === 2) { lat = parseFloat(parts[0]); lng = parseFloat(parts[1]); } } else if (settings.latitude && settings.longitude) { lat = parseFloat(settings.latitude); lng = parseFloat(settings.longitude); } zoom = parseInt(settings.zoom) || config.map.defaultZoom; if (lat && lng && !isNaN(lat) && !isNaN(lng)) { return res.json({ success: true, location: { latitude: lat, longitude: lng, zoom: zoom }, source: 'database', settingsId: extractId(settings), lastUpdated: settings.created_at }); } } // Return defaults res.json({ success: true, location: { latitude: config.map.defaultLat, longitude: config.map.defaultLng, zoom: config.map.defaultZoom }, source: 'defaults' }); } catch (error) { logger.error('Error fetching start location:', error); // Return defaults on error res.json({ success: true, location: { latitude: config.map.defaultLat, longitude: config.map.defaultLng, zoom: config.map.defaultZoom }, source: 'defaults' }); } } async updateStartLocation(req, res) { try { const { latitude, longitude, zoom } = req.body; // Validate input if (!latitude || !longitude) { return res.status(400).json({ success: false, error: 'Latitude and longitude are required' }); } const lat = parseFloat(latitude); const lng = parseFloat(longitude); const mapZoom = parseInt(zoom) || 11; if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) { return res.status(400).json({ success: false, error: 'Invalid coordinates' }); } if (!config.nocodb.settingsSheetId) { return res.status(500).json({ success: false, error: 'Settings sheet not configured' }); } // Get current settings to preserve other fields let currentConfig = {}; try { currentConfig = await nocodbService.getLatestSettings() || {}; // Debug logging to see what we're getting logger.info('Retrieved current config:', { id: currentConfig.Id || currentConfig.ID || currentConfig.id, walk_sheet_title: currentConfig.walk_sheet_title, walk_sheet_subtitle: currentConfig.walk_sheet_subtitle, walk_sheet_footer: currentConfig.walk_sheet_footer, hasFooter: !!currentConfig.walk_sheet_footer, footerType: typeof currentConfig.walk_sheet_footer, allKeys: Object.keys(currentConfig) }); } catch (error) { logger.warn('Could not retrieve current settings for preservation, using defaults:', error.message); currentConfig = {}; } // Create new settings row - use values directly without || operator const walkSheetConfig = extractWalkSheetConfig(currentConfig, SettingsController.defaultSettings); const settingData = { created_at: new Date().toISOString(), created_by: req.session.userEmail, // Map location fields (what we're updating) 'Geo-Location': `${lat};${lng}`, latitude: lat, longitude: lng, zoom: mapZoom, // Preserve walk sheet fields using helper function ...walkSheetConfig }; logger.info('Creating settings row with data:', { walk_sheet_footer: settingData.walk_sheet_footer, footerLength: settingData.walk_sheet_footer?.length }); const response = await nocodbService.create( config.nocodb.settingsSheetId, settingData ); logger.info('Created new settings row with start location'); res.json({ success: true, message: 'Start location saved successfully', location: { latitude: lat, longitude: lng, zoom: mapZoom }, settingsId: extractId(response) }); } catch (error) { logger.error('Error updating start location:', error); res.status(500).json({ success: false, error: error.message || 'Failed to update start location' }); } } async getWalkSheetConfig(req, res) { try { if (!config.nocodb.settingsSheetId) { logger.warn('SETTINGS_SHEET_ID not configured, returning defaults'); return res.json({ success: true, config: SettingsController.defaultSettings, source: 'defaults', message: 'Settings sheet not configured, using defaults' }); } const settings = await nocodbService.getLatestSettings(); if (!settings) { logger.info('No settings found in database, returning defaults'); return res.json({ success: true, config: SettingsController.defaultSettings, source: 'defaults', message: 'No settings found in database' }); } const walkSheetConfig = extractWalkSheetConfig(settings, SettingsController.defaultSettings); logger.info(`Retrieved walk sheet config from database (ID: ${extractId(settings)})`); res.json({ success: true, config: walkSheetConfig, source: 'database', settingsId: extractId(settings), lastUpdated: settings.created_at || settings.updated_at }); } catch (error) { logger.error('Failed to get walk sheet config:', error); // Return defaults if there's an error res.json({ success: true, config: SettingsController.defaultSettings, source: 'defaults', message: 'Error retrieving from database, using defaults', error: error.message }); } } async updateWalkSheetConfig(req, res) { try { if (!config.nocodb.settingsSheetId) { return res.status(500).json({ success: false, error: 'Settings sheet not configured' }); } const configData = req.body; logger.info('Received walk sheet config:', JSON.stringify(configData, null, 2)); // Validate input if (!configData || typeof configData !== 'object') { return res.status(400).json({ success: false, error: 'Invalid configuration data' }); } // Get current settings to preserve other fields let currentConfig = {}; try { currentConfig = await nocodbService.getLatestSettings() || {}; } catch (error) { logger.warn('Could not retrieve current settings for preservation, using defaults:', error.message); currentConfig = {}; } const userEmail = req.session.userEmail; const timestamp = new Date().toISOString(); // Prepare data for saving const walkSheetData = { created_at: timestamp, created_by: userEmail, // Preserve map location fields with consistent fallbacks 'Geo-Location': currentConfig['Geo-Location'] || currentConfig.geodata || `${config.map.defaultLat};${config.map.defaultLng}`, latitude: currentConfig.latitude || config.map.defaultLat, longitude: currentConfig.longitude || config.map.defaultLng, zoom: currentConfig.zoom || config.map.defaultZoom, // Walk sheet fields (what we're updating) walk_sheet_title: (configData.walk_sheet_title || '').toString().trim(), walk_sheet_subtitle: (configData.walk_sheet_subtitle || '').toString().trim(), walk_sheet_footer: (configData.walk_sheet_footer || '').toString().trim(), 'Walk Sheet Title': (configData.walk_sheet_title || '').toString().trim(), 'Walk Sheet Subtitle': (configData.walk_sheet_subtitle || '').toString().trim(), 'Walk Sheet Footer': (configData.walk_sheet_footer || '').toString().trim(), qr_code_1_url: validateUrl(configData.qr_code_1_url), qr_code_1_label: (configData.qr_code_1_label || '').toString().trim(), qr_code_2_url: validateUrl(configData.qr_code_2_url), qr_code_2_label: (configData.qr_code_2_label || '').toString().trim(), qr_code_3_url: validateUrl(configData.qr_code_3_url), qr_code_3_label: (configData.qr_code_3_label || '').toString().trim(), 'QR Code 1 URL': validateUrl(configData.qr_code_1_url), 'QR Code 1 Label': (configData.qr_code_1_label || '').toString().trim(), 'QR Code 2 URL': validateUrl(configData.qr_code_2_url), 'QR Code 2 Label': (configData.qr_code_2_label || '').toString().trim(), 'QR Code 3 URL': validateUrl(configData.qr_code_3_url), 'QR Code 3 Label': (configData.qr_code_3_label || '').toString().trim() }; const response = await nocodbService.create( config.nocodb.settingsSheetId, walkSheetData ); const newId = extractId(response); res.json({ success: true, message: 'Walk sheet configuration saved successfully', config: walkSheetData, settingsId: newId, timestamp: timestamp }); } catch (error) { logger.error('Failed to save walk sheet config:', error); logger.error('Error response:', error.response?.data); let errorMessage = 'Failed to save walk sheet configuration'; let errorDetails = null; if (error.response?.data) { if (error.response.data.message) { errorMessage = error.response.data.message; } if (error.response.data.errors) { errorDetails = error.response.data.errors; } } res.status(500).json({ success: false, error: errorMessage, details: errorDetails, timestamp: new Date().toISOString() }); } } // Public endpoint for start location (no auth required) async getPublicStartLocation(req, res) { try { const settings = await nocodbService.getLatestSettings(); if (settings) { let lat, lng, zoom; if (settings['Geo-Location']) { const parts = settings['Geo-Location'].split(';'); if (parts.length === 2) { lat = parseFloat(parts[0]); lng = parseFloat(parts[1]); } } else if (settings.latitude && settings.longitude) { lat = parseFloat(settings.latitude); lng = parseFloat(settings.longitude); } zoom = parseInt(settings.zoom) || config.map.defaultZoom; if (lat && lng && !isNaN(lat) && !isNaN(lng)) { logger.info(`Returning location from database: ${lat}, ${lng}, zoom: ${zoom}`); return res.json({ latitude: lat, longitude: lng, zoom: zoom }); } } } catch (error) { logger.error('Error fetching config start location:', error); } // Return defaults logger.info(`Using default start location: ${config.map.defaultLat}, ${config.map.defaultLng}, zoom: ${config.map.defaultZoom}`); res.json({ latitude: config.map.defaultLat, longitude: config.map.defaultLng, zoom: config.map.defaultZoom }); } } module.exports = new SettingsController();