const nocodbService = require('../services/nocodb'); const logger = require('../utils/logger'); const config = require('../config'); const listmonkService = require('../services/listmonk'); const { syncGeoFields, validateCoordinates, checkBounds, extractId } = require('../utils/helpers'); class LocationsController { async getAll(req, res) { try { const { limit, offset = 0, where } = req.query; const params = { offset }; // Only add limit if explicitly provided in query if (limit !== undefined) { params.limit = limit; } if (where) params.where = where; logger.info('Fetching locations from NocoDB'); const response = await nocodbService.getLocations(params); const locations = response.list || []; // Process and validate locations const validLocations = locations.filter(loc => { loc = syncGeoFields(loc); if (loc.latitude && loc.longitude) { return true; } // Try to parse from geodata column if (loc.geodata && typeof loc.geodata === 'string') { const parts = loc.geodata.split(';'); if (parts.length === 2) { loc.latitude = parseFloat(parts[0]); loc.longitude = parseFloat(parts[1]); return !isNaN(loc.latitude) && !isNaN(loc.longitude); } } return false; }); logger.info(`Retrieved ${validLocations.length} valid locations out of ${locations.length} total`); // Check if user is temp user and limit data accordingly if (req.session?.userType === 'temp') { // For temp users, return limited data but include necessary fields for functionality const limitedLocations = validLocations.map(loc => { const locationId = loc.id || loc.Id || loc.ID || loc._id; return { // Include ID with all possible variants for compatibility id: locationId, Id: locationId, ID: locationId, _id: locationId, 'Geo-Location': loc['Geo-Location'], latitude: loc.latitude, longitude: loc.longitude, // Include display fields needed for map functionality 'First Name': loc['First Name'] || '', 'Last Name': loc['Last Name'] || '', // Include last name for display 'Support Level': loc['Support Level'], Address: loc.Address || '', 'Unit Number': loc['Unit Number'] || '', Notes: loc.Notes || '', Sign: loc.Sign, 'Sign Size': loc['Sign Size'] || '', // Exclude sensitive fields like Email, Phone }; }); logger.info(`Returning limited data for temp user: ${limitedLocations.length} locations`); return res.json({ success: true, count: limitedLocations.length, total: response.pageInfo?.totalRows || limitedLocations.length, locations: limitedLocations, isLimited: true // Flag to indicate limited data }); } res.json({ success: true, count: validLocations.length, total: response.pageInfo?.totalRows || validLocations.length, locations: validLocations }); } catch (error) { logger.error('Error fetching locations:', error.message); if (error.response) { res.status(error.response.status).json({ success: false, error: 'Failed to fetch data from NocoDB', details: error.response.data }); } else if (error.code === 'ECONNABORTED') { res.status(504).json({ success: false, error: 'Request timeout' }); } else { res.status(500).json({ success: false, error: 'Internal server error' }); } } } async getById(req, res) { try { const location = await nocodbService.getById( config.nocodb.tableId, req.params.id ); res.json({ success: true, location }); } catch (error) { logger.error(`Error fetching location ${req.params.id}:`, error.message); res.status(error.response?.status || 500).json({ success: false, error: 'Failed to fetch location' }); } } async create(req, res) { try { // Add debugging logs logger.info('Session data:', { authenticated: req.session.authenticated, userId: req.session.userId, userEmail: req.session.userEmail, isAdmin: req.session.isAdmin }); let locationData = { ...req.body }; locationData = syncGeoFields(locationData); const { latitude, longitude, ...additionalData } = locationData; // Validate coordinates const validation = validateCoordinates(latitude, longitude); if (!validation.valid) { return res.status(400).json({ success: false, error: validation.error }); } // Check bounds if configured if (config.map.bounds) { const boundsCheck = checkBounds(validation.latitude, validation.longitude); if (!boundsCheck.valid) { return res.status(400).json({ success: false, error: boundsCheck.error }); } } // Format geodata with string values to preserve precision const geodata = `${validation.latitude};${validation.longitude}`; // Prepare data for NocoDB - keep coordinates as strings const finalData = { geodata, 'Geo-Location': geodata, latitude: validation.latitude, longitude: validation.longitude, ...additionalData, created_by_user: req.session.userEmail || 'anonymous' // Add fallback }; logger.info('Final data being sent to NocoDB:', finalData); logger.info('Creating new location:', { lat: validation.latitude, lng: validation.longitude, user: req.session.userEmail }); const response = await nocodbService.create( config.nocodb.tableId, finalData ); logger.info('Location created successfully:', extractId(response)); // Real-time sync to Listmonk (async, don't block response) if (listmonkService.syncEnabled && response.Email) { setImmediate(async () => { try { const syncResult = await listmonkService.syncLocation(response); if (!syncResult.success) { logger.warn('Listmonk sync failed for new location', { locationId: extractId(response), email: response.Email, error: syncResult.error }); } else { logger.debug('Location synced to Listmonk', { locationId: extractId(response), email: response.Email }); } } catch (error) { logger.error('Listmonk sync error for new location', { locationId: extractId(response), error: error.message }); } }); } res.status(201).json({ success: true, location: response }); } catch (error) { logger.error('Error creating location:', error.message); if (error.response) { res.status(error.response.status).json({ success: false, error: 'Failed to save location to NocoDB', details: error.response.data }); } else { res.status(500).json({ success: false, error: 'Internal server error' }); } } } async update(req, res) { try { const locationId = req.params.id; // Validate ID if (!locationId || locationId === 'undefined' || locationId === 'null') { return res.status(400).json({ success: false, error: 'Invalid location ID' }); } let updateData = { ...req.body }; // Remove ID fields to avoid conflicts delete updateData.ID; delete updateData.Id; delete updateData.id; delete updateData._id; // Sync geo fields updateData = syncGeoFields(updateData); // Add update tracking updateData.last_updated_by_user = req.session.userEmail; // Changed from last_updated_by logger.info(`Updating location ${locationId}`, { user: req.session.userEmail }); const response = await nocodbService.update( config.nocodb.tableId, locationId, updateData ); logger.info('Location updated successfully:', locationId); // Real-time sync to Listmonk (async, don't block response) if (listmonkService.syncEnabled && response.Email) { setImmediate(async () => { try { const syncResult = await listmonkService.syncLocation(response); if (!syncResult.success) { logger.warn('Listmonk sync failed for updated location', { locationId: locationId, email: response.Email, error: syncResult.error }); } else { logger.debug('Updated location synced to Listmonk', { locationId: locationId, email: response.Email }); } } catch (error) { logger.error('Listmonk sync error for updated location', { locationId: locationId, error: error.message }); } }); } res.json({ success: true, location: response }); } catch (error) { logger.error(`Error updating location ${req.params.id}:`, error.message); res.status(error.response?.status || 500).json({ success: false, error: 'Failed to update location', details: error.response?.data?.message || error.message }); } } async delete(req, res) { try { // Check if user is temp and deny delete if (req.session?.userType === 'temp') { return res.status(403).json({ success: false, error: 'Temporary users cannot delete locations' }); } const locationId = req.params.id; // Validate ID if (!locationId || locationId === 'undefined' || locationId === 'null') { return res.status(400).json({ success: false, error: 'Invalid location ID' }); } // Get location data before deletion (for Listmonk cleanup) let locationData = null; if (listmonkService.syncEnabled) { try { const getResponse = await nocodbService.getById(config.nocodb.tableId, locationId); locationData = getResponse; } catch (error) { logger.warn('Could not fetch location data before deletion', error.message); } } await nocodbService.delete( config.nocodb.tableId, locationId ); logger.info(`Location ${locationId} deleted by ${req.session.userEmail}`); // Remove from Listmonk (async, don't block response) if (listmonkService.syncEnabled && locationData && locationData.Email) { setImmediate(async () => { try { const syncResult = await listmonkService.removeSubscriber(locationData.Email); if (!syncResult.success) { logger.warn('Failed to remove deleted location from Listmonk', { locationId: locationId, email: locationData.Email, error: syncResult.error }); } else { logger.debug('Deleted location removed from Listmonk', { locationId: locationId, email: locationData.Email }); } } catch (error) { logger.error('Listmonk cleanup error for deleted location', { locationId: locationId, error: error.message }); } }); } res.json({ success: true, message: 'Location deleted successfully' }); } catch (error) { logger.error(`Error deleting location ${req.params.id}:`, error.message); res.status(error.response?.status || 500).json({ success: false, error: 'Failed to delete location' }); } } } module.exports = new LocationsController();