const nocodbService = require('../services/nocodb'); const config = require('../config'); const logger = require('../utils/logger'); const { extractId } = require('../utils/helpers'); class ShiftsController { // Get all shifts (public) async getAll(req, res) { try { if (!config.nocodb.shiftsSheetId) { return res.status(500).json({ success: false, error: 'Shifts not configured' }); } logger.info('Loading public shifts from:', config.nocodb.shiftsSheetId); // Load all shifts without filter - we'll filter in JavaScript const response = await nocodbService.getAll(config.nocodb.shiftsSheetId, { sort: 'Date,Start Time' }); logger.info('Loaded shifts:', response); // Filter out cancelled shifts manually const shifts = (response.list || []).filter(shift => shift.Status !== 'Cancelled' ); res.json({ success: true, shifts: shifts }); } catch (error) { logger.error('Error fetching shifts:', error); res.status(500).json({ success: false, error: 'Failed to fetch shifts' }); } } // Get user's signups async getUserSignups(req, res) { try { const userEmail = req.session.userEmail; // Check if shift signups sheet is configured if (!config.nocodb.shiftSignupsSheetId) { logger.warn('Shift signups sheet not configured'); return res.json({ success: true, signups: [] }); } // Load all signups and filter in JavaScript const allSignups = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId, { sort: '-Signup Date' }); logger.info('All signups loaded:', allSignups); logger.info('Filtering for user:', userEmail); // Filter for this user's confirmed signups const userSignups = (allSignups.list || []).filter(signup => { logger.debug('Checking signup:', signup); // NocoDB returns fields with title case const email = signup['User Email']; const status = signup.Status; logger.debug(`Comparing: email="${email}" vs userEmail="${userEmail}", status="${status}"`); return email === userEmail && status === 'Confirmed'; }); logger.info('User signups found:', userSignups); // Transform to match expected format in frontend const transformedSignups = userSignups.map(signup => ({ id: signup.ID || signup.id, shift_id: signup['Shift ID'], user_email: signup['User Email'], user_name: signup['User Name'], signup_date: signup['Signup Date'], status: signup.Status })); res.json({ success: true, signups: transformedSignups }); } catch (error) { logger.error('Error fetching user signups:', error); // Don't fail, just return empty array res.json({ success: true, signups: [] }); } } // Sign up for a shift async signup(req, res) { try { if (!config.nocodb.shiftsSheetId) { return res.status(400).json({ success: false, error: 'Shifts sheet not configured' }); } if (!config.nocodb.shiftSignupsSheetId) { return res.status(400).json({ success: false, error: 'Shift signups sheet not configured' }); } const { shiftId } = req.params; const userEmail = req.session.userEmail; const userName = req.session.userName || userEmail; logger.info(`User ${userEmail} attempting to sign up for shift ${shiftId}`); // Check if shift exists and is open const shift = await nocodbService.getById(config.nocodb.shiftsSheetId, shiftId); if (!shift || shift.Status === 'Cancelled') { return res.status(400).json({ success: false, error: 'Shift not available' }); } if (shift['Current Volunteers'] >= shift['Max Volunteers']) { return res.status(400).json({ success: false, error: 'Shift is full' }); } // Check if already signed up - get all signups and filter const allSignups = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId); const existingSignup = (allSignups.list || []).find(signup => { return signup['Shift ID'] === parseInt(shiftId) && signup['User Email'] === userEmail && signup.Status === 'Confirmed'; }); if (existingSignup) { return res.status(400).json({ success: false, error: 'Already signed up for this shift' }); } // Create signup const signup = await nocodbService.create(config.nocodb.shiftSignupsSheetId, { 'Shift ID': parseInt(shiftId), 'User Email': userEmail, 'User Name': userName, 'Signup Date': new Date().toISOString(), 'Status': 'Confirmed' }); logger.info('Created signup:', signup); // Update shift volunteer count await nocodbService.update(config.nocodb.shiftsSheetId, shiftId, { 'Current Volunteers': (shift['Current Volunteers'] || 0) + 1, 'Status': shift['Current Volunteers'] + 1 >= shift['Max Volunteers'] ? 'Full' : 'Open' }); res.json({ success: true, message: 'Successfully signed up for shift' }); } catch (error) { logger.error('Error signing up for shift:', error); res.status(500).json({ success: false, error: 'Failed to sign up for shift' }); } } // Cancel shift signup async cancelSignup(req, res) { try { if (!config.nocodb.shiftsSheetId || !config.nocodb.shiftSignupsSheetId) { return res.status(400).json({ success: false, error: 'Shifts not configured' }); } const { shiftId } = req.params; const userEmail = req.session.userEmail; logger.info(`User ${userEmail} attempting to cancel signup for shift ${shiftId}`); // Find the signup const allSignups = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId); const signup = (allSignups.list || []).find(s => { return s['Shift ID'] === parseInt(shiftId) && s['User Email'] === userEmail && s.Status === 'Confirmed'; }); if (!signup) { return res.status(404).json({ success: false, error: 'Signup not found' }); } // Update signup status to cancelled await nocodbService.update(config.nocodb.shiftSignupsSheetId, signup.ID || signup.id, { 'Status': 'Cancelled' }); // Update shift volunteer count const shift = await nocodbService.getById(config.nocodb.shiftsSheetId, shiftId); const newCount = Math.max(0, (shift['Current Volunteers'] || 0) - 1); await nocodbService.update(config.nocodb.shiftsSheetId, shiftId, { 'Current Volunteers': newCount, 'Status': newCount >= shift['Max Volunteers'] ? 'Full' : 'Open' }); res.json({ success: true, message: 'Successfully cancelled signup' }); } catch (error) { logger.error('Error cancelling signup:', error); res.status(500).json({ success: false, error: 'Failed to cancel signup' }); } } // Admin: Create shift async create(req, res) { try { const { title, description, date, startTime, endTime, location, maxVolunteers } = req.body; if (!title || !date || !startTime || !endTime || !location || !maxVolunteers) { return res.status(400).json({ success: false, error: 'Missing required fields' }); } const shift = await nocodbService.create(config.nocodb.shiftsSheetId, { Title: title, Description: description, Date: date, 'Start Time': startTime, 'End Time': endTime, Location: location, 'Max Volunteers': parseInt(maxVolunteers), 'Current Volunteers': 0, Status: 'Open', 'Created By': req.session.userEmail, 'Created At': new Date().toISOString(), 'Updated At': new Date().toISOString() }); res.json({ success: true, shift }); } catch (error) { logger.error('Error creating shift:', error); res.status(500).json({ success: false, error: 'Failed to create shift' }); } } // Admin: Update shift async update(req, res) { try { const { id } = req.params; const updateData = {}; // Map fields that can be updated const fieldMap = { title: 'Title', description: 'Description', date: 'Date', startTime: 'Start Time', endTime: 'End Time', location: 'Location', maxVolunteers: 'Max Volunteers', status: 'Status' }; for (const [key, field] of Object.entries(fieldMap)) { if (req.body[key] !== undefined) { updateData[field] = req.body[key]; } } if (updateData['Max Volunteers']) { updateData['Max Volunteers'] = parseInt(updateData['Max Volunteers']); } updateData['Updated At'] = new Date().toISOString(); const updated = await nocodbService.update(config.nocodb.shiftsSheetId, id, updateData); res.json({ success: true, shift: updated }); } catch (error) { logger.error('Error updating shift:', error); res.status(500).json({ success: false, error: 'Failed to update shift' }); } } // Admin: Delete shift async delete(req, res) { try { const { id } = req.params; // Check if signups sheet is configured if (config.nocodb.shiftSignupsSheetId) { try { // Get all signups and filter in JavaScript to avoid NocoDB query issues const allSignups = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId); // Filter for confirmed signups for this shift const signupsToCancel = (allSignups.list || []).filter(signup => signup['Shift ID'] === parseInt(id) && signup.Status === 'Confirmed' ); // Cancel each signup for (const signup of signupsToCancel) { await nocodbService.update(config.nocodb.shiftSignupsSheetId, signup.ID || signup.id, { Status: 'Cancelled' }); } logger.info(`Cancelled ${signupsToCancel.length} signups for shift ${id}`); } catch (signupError) { logger.error('Error cancelling signups:', signupError); // Continue with shift deletion even if signup cancellation fails } } // Delete the shift await nocodbService.delete(config.nocodb.shiftsSheetId, id); res.json({ success: true, message: 'Shift deleted successfully' }); } catch (error) { logger.error('Error deleting shift:', error); res.status(500).json({ success: false, error: 'Failed to delete shift' }); } } // Admin: Get all shifts with signup details async getAllAdmin(req, res) { try { if (!config.nocodb.shiftsSheetId) { logger.error('Shifts sheet not configured'); return res.status(500).json({ success: false, error: 'Shifts not configured' }); } logger.info('Loading admin shifts from:', config.nocodb.shiftsSheetId); let shifts; try { shifts = await nocodbService.getAll(config.nocodb.shiftsSheetId, { sort: '-Date,-Start Time' }); } catch (apiError) { logger.error('Error loading shifts from NocoDB:', apiError); // If it's a 422 error, try without sort parameters if (apiError.response?.status === 422) { logger.warn('Retrying without sort parameters due to 422 error'); try { shifts = await nocodbService.getAll(config.nocodb.shiftsSheetId); } catch (retryError) { logger.error('Retry also failed:', retryError); return res.status(500).json({ success: false, error: 'Failed to load shifts from database' }); } } else { throw apiError; } } logger.info('Loaded shifts:', shifts); // Only try to get signups if the signups sheet is configured if (config.nocodb.shiftSignupsSheetId) { // Get signup counts for each shift for (const shift of shifts.list || []) { try { const signups = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId); // Filter signups for this shift manually const shiftSignups = (signups.list || []).filter(signup => signup['Shift ID'] === shift.ID && signup.Status === 'Confirmed' ); shift.signups = shiftSignups; } catch (signupError) { logger.error(`Error loading signups for shift ${shift.ID}:`, signupError); shift.signups = []; } } } else { logger.warn('Shift signups sheet not configured, skipping signup data'); // Set empty signups for all shifts for (const shift of shifts.list || []) { shift.signups = []; } } res.json({ success: true, shifts: shifts.list || [] }); } catch (error) { logger.error('Error fetching admin shifts:', error); res.status(500).json({ success: false, error: 'Failed to fetch shifts' }); } } } module.exports = new ShiftsController();