freealberta/map/app/controllers/locationsController.js

414 lines
16 KiB
JavaScript

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();