271 lines
10 KiB
JavaScript
271 lines
10 KiB
JavaScript
const nocodbService = require('../services/nocodb');
|
|
const config = require('../config');
|
|
const logger = require('../utils/logger');
|
|
const { sendEmail } = require('../services/email');
|
|
const emailTemplates = require('../services/emailTemplates');
|
|
const crypto = require('crypto');
|
|
|
|
class PublicShiftsController {
|
|
// Get all public shifts (without volunteer counts)
|
|
async getPublicShifts(req, res) {
|
|
try {
|
|
if (!config.nocodb.shiftsSheetId) {
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: 'Shifts not configured'
|
|
});
|
|
}
|
|
|
|
const response = await nocodbService.getAll(config.nocodb.shiftsSheetId, {
|
|
sort: 'Date,Start Time'
|
|
});
|
|
|
|
// More flexible filtering - check for Public field being truthy or not explicitly false
|
|
const shifts = (response.list || []).filter(shift => {
|
|
// Skip cancelled shifts
|
|
if (shift.Status === 'Cancelled') {
|
|
return false;
|
|
}
|
|
|
|
// If Public field doesn't exist, include the shift (backwards compatibility)
|
|
if (shift.Public === undefined || shift.Public === null) {
|
|
logger.info(`Shift ${shift.Title} has no Public field, including it`);
|
|
return true;
|
|
}
|
|
|
|
// Check for various truthy values (true, "true", 1, "1", "yes", etc.)
|
|
const publicValue = String(shift.Public).toLowerCase();
|
|
return publicValue === 'true' || publicValue === '1' || publicValue === 'yes';
|
|
});
|
|
|
|
logger.info(`Found ${shifts.length} public shifts out of ${response.list?.length || 0} total`);
|
|
|
|
res.json({
|
|
success: true,
|
|
shifts: shifts
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error fetching public shifts:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch shifts'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Get single shift details for direct linking
|
|
async getShiftById(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const shift = await nocodbService.getById(config.nocodb.shiftsSheetId, id);
|
|
|
|
if (!shift || shift.Status === 'Cancelled') {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Shift not found'
|
|
});
|
|
}
|
|
|
|
// Similar flexible check for single shift
|
|
if (shift.Public !== undefined && shift.Public !== null) {
|
|
const publicValue = String(shift.Public).toLowerCase();
|
|
const isPublic = publicValue === 'true' || publicValue === '1' || publicValue === 'yes';
|
|
if (!isPublic) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Shift not found'
|
|
});
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
shift
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error fetching shift:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch shift'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Public signup - creates temp user and signs them up
|
|
async publicSignup(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { email, name, phone } = req.body;
|
|
|
|
if (!email || !name) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Email and name are required'
|
|
});
|
|
}
|
|
|
|
// Get shift details
|
|
const shift = await nocodbService.getById(config.nocodb.shiftsSheetId, id);
|
|
logger.info('Raw shift data retrieved:', JSON.stringify(shift, null, 2));
|
|
|
|
if (!shift || shift.Status === 'Cancelled') {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Shift not found or cancelled'
|
|
});
|
|
}
|
|
|
|
// Check if shift is full
|
|
if (shift['Current Volunteers'] >= shift['Max Volunteers']) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'This shift is full'
|
|
});
|
|
}
|
|
|
|
// Check if user exists
|
|
let user = await nocodbService.getUserByEmail(email);
|
|
let isNewUser = false;
|
|
let tempPassword = null;
|
|
|
|
if (!user) {
|
|
// Generate temp password using instance method
|
|
const controller = new PublicShiftsController();
|
|
tempPassword = controller.generateTempPassword();
|
|
|
|
const shiftDate = new Date(shift.Date);
|
|
const expiresAt = new Date(shiftDate);
|
|
expiresAt.setDate(expiresAt.getDate() + 1); // Expires day after shift
|
|
|
|
const userData = {
|
|
Email: email,
|
|
Password: tempPassword,
|
|
Name: name,
|
|
UserType: 'temp',
|
|
'User Type': 'temp',
|
|
ExpiresAt: expiresAt.toISOString()
|
|
};
|
|
|
|
logger.info('Creating temp user with data:', JSON.stringify(userData, null, 2));
|
|
user = await nocodbService.create(config.nocodb.loginSheetId, userData);
|
|
isNewUser = true;
|
|
logger.info(`Created temp user ${email} for shift ${id}`);
|
|
}
|
|
|
|
// Check if already signed up
|
|
const allSignups = await nocodbService.getAllPaginated(config.nocodb.shiftSignupsSheetId);
|
|
const existingSignup = (allSignups.list || []).find(s =>
|
|
s['Shift ID'] === parseInt(id) &&
|
|
s['User Email'] === email &&
|
|
s.Status === 'Confirmed'
|
|
);
|
|
|
|
if (existingSignup) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'You are already signed up for this shift'
|
|
});
|
|
}
|
|
|
|
// Create signup
|
|
const signupData = {
|
|
'Shift ID': parseInt(id),
|
|
'Shift Title': shift.Title,
|
|
'User Email': email,
|
|
'User Name': name,
|
|
'Signup Date': new Date().toISOString(),
|
|
'Status': 'Confirmed',
|
|
'Source': 'public'
|
|
};
|
|
|
|
// Add phone if provided
|
|
if (phone && phone.trim()) {
|
|
signupData['Phone'] = phone.trim();
|
|
}
|
|
|
|
logger.info('Creating shift signup with data:', JSON.stringify(signupData, null, 2));
|
|
const signup = await nocodbService.create(config.nocodb.shiftSignupsSheetId, signupData);
|
|
|
|
// Update shift volunteer count
|
|
const newCount = (shift['Current Volunteers'] || 0) + 1;
|
|
await nocodbService.update(config.nocodb.shiftsSheetId, id, {
|
|
'Current Volunteers': newCount,
|
|
'Status': newCount >= shift['Max Volunteers'] ? 'Full' : 'Open'
|
|
});
|
|
|
|
// Send confirmation email
|
|
const controller = new PublicShiftsController();
|
|
await controller.sendSignupConfirmation(email, name, shift, isNewUser, tempPassword);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Successfully signed up! Check your email for confirmation and login details.',
|
|
isNewUser
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error in public signup:', error);
|
|
logger.error('Error details:', error.response?.data || error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to complete signup. Please try again.'
|
|
});
|
|
}
|
|
}
|
|
|
|
generateTempPassword() {
|
|
// Generate readable temporary password
|
|
const adjectives = ['Blue', 'Green', 'Happy', 'Swift', 'Bright'];
|
|
const nouns = ['Tiger', 'Eagle', 'River', 'Mountain', 'Star'];
|
|
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
const num = Math.floor(Math.random() * 100);
|
|
return `${adj}${noun}${num}`;
|
|
}
|
|
|
|
async sendSignupConfirmation(email, name, shift, isNewUser, tempPassword) {
|
|
const baseUrl = config.isProduction ?
|
|
`https://map.${config.domain}` :
|
|
`http://localhost:${config.port}`;
|
|
|
|
const shiftDate = new Date(shift.Date);
|
|
|
|
// Prepare all variables including optional ones
|
|
const variables = {
|
|
APP_NAME: process.env.APP_NAME || 'CMlite Map',
|
|
USER_NAME: name,
|
|
USER_EMAIL: email,
|
|
SHIFT_TITLE: shift.Title || 'Untitled Shift',
|
|
SHIFT_DATE: shiftDate.toLocaleDateString(),
|
|
SHIFT_TIME: `${shift['Start Time'] || ''} - ${shift['End Time'] || ''}`,
|
|
SHIFT_LOCATION: shift.Location || 'Location TBD',
|
|
SHIFT_DESCRIPTION: shift.Description || '', // Include even if empty
|
|
LOGIN_URL: `${baseUrl}/login.html`,
|
|
SHIFTS_URL: `${baseUrl}/shifts.html`,
|
|
IS_NEW_USER: isNewUser,
|
|
TEMP_PASSWORD: tempPassword || '',
|
|
TIMESTAMP: new Date().toLocaleString()
|
|
};
|
|
|
|
// Log the variables for debugging
|
|
logger.info('Email template variables:', JSON.stringify(variables, null, 2));
|
|
|
|
const templateName = isNewUser ? 'public-shift-signup-new' : 'public-shift-signup-existing';
|
|
const { html, text } = await emailTemplates.render(templateName, variables);
|
|
|
|
return await sendEmail({
|
|
to: email,
|
|
subject: `Shift Signup Confirmation - ${shift.Title}`,
|
|
text,
|
|
html
|
|
});
|
|
}
|
|
}
|
|
|
|
// Create singleton instance and bind methods
|
|
const controller = new PublicShiftsController();
|
|
module.exports = {
|
|
getPublicShifts: controller.getPublicShifts.bind(controller),
|
|
getShiftById: controller.getShiftById.bind(controller),
|
|
publicSignup: controller.publicSignup.bind(controller)
|
|
}; |