483 lines
17 KiB
JavaScript
483 lines
17 KiB
JavaScript
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);
|
|
|
|
const response = await nocodbService.getAll(config.nocodb.shiftsSheetId, {
|
|
sort: 'Date,Start Time'
|
|
});
|
|
|
|
let shifts = (response.list || []).filter(shift =>
|
|
shift.Status !== 'Cancelled'
|
|
);
|
|
|
|
// If signups sheet is configured, calculate current volunteer counts
|
|
if (config.nocodb.shiftSignupsSheetId) {
|
|
try {
|
|
const signupsResponse = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId);
|
|
const allSignups = signupsResponse.list || [];
|
|
|
|
// Update each shift with calculated volunteer count
|
|
shifts = shifts.map(shift => {
|
|
const confirmedSignups = allSignups.filter(signup =>
|
|
signup['Shift ID'] === shift.ID && signup.Status === 'Confirmed'
|
|
);
|
|
|
|
const currentVolunteers = confirmedSignups.length;
|
|
const maxVolunteers = shift['Max Volunteers'] || 0;
|
|
|
|
return {
|
|
...shift,
|
|
'Current Volunteers': currentVolunteers,
|
|
'Status': currentVolunteers >= maxVolunteers ? 'Full' : 'Open'
|
|
};
|
|
});
|
|
} catch (signupError) {
|
|
logger.warn('Could not load signups for volunteer count calculation:', signupError);
|
|
}
|
|
}
|
|
|
|
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(); |