A couple more fast email buttons
This commit is contained in:
@@ -494,6 +494,302 @@ class ShiftsController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Admin: Add user to shift
|
||||
async addUserToShift(req, res) {
|
||||
try {
|
||||
const { shiftId } = req.params;
|
||||
const { userEmail } = req.body;
|
||||
|
||||
if (!userEmail) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'User email is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.nocodb.shiftsSheetId || !config.nocodb.shiftSignupsSheetId) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Shifts not properly configured'
|
||||
});
|
||||
}
|
||||
|
||||
// Get shift details
|
||||
const shift = await nocodbService.getById(config.nocodb.shiftsSheetId, shiftId);
|
||||
if (!shift) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Shift not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
const user = await nocodbService.getUserByEmail(userEmail);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'User not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is already signed up
|
||||
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: 'User is already signed up for this shift'
|
||||
});
|
||||
}
|
||||
|
||||
// Check capacity
|
||||
const confirmedSignups = (allSignups.list || []).filter(signup =>
|
||||
signup['Shift ID'] === parseInt(shiftId) && signup.Status === 'Confirmed'
|
||||
);
|
||||
|
||||
if (confirmedSignups.length >= shift['Max Volunteers']) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Shift is at maximum capacity'
|
||||
});
|
||||
}
|
||||
|
||||
// Create signup
|
||||
const signup = await nocodbService.create(config.nocodb.shiftSignupsSheetId, {
|
||||
'Shift ID': parseInt(shiftId),
|
||||
'Shift Title': shift.Title,
|
||||
'User Email': userEmail,
|
||||
'User Name': user.Name || user.name || userEmail,
|
||||
'Signup Date': new Date().toISOString(),
|
||||
'Status': 'Confirmed'
|
||||
});
|
||||
|
||||
// Update shift volunteer count
|
||||
const newCount = confirmedSignups.length + 1;
|
||||
await nocodbService.update(config.nocodb.shiftsSheetId, shiftId, {
|
||||
'Current Volunteers': newCount,
|
||||
'Status': newCount >= shift['Max Volunteers'] ? 'Full' : 'Open'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'User successfully added to shift',
|
||||
signup: signup
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error adding user to shift:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to add user to shift'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Admin: Remove user from shift
|
||||
async removeUserFromShift(req, res) {
|
||||
try {
|
||||
const { shiftId, userId } = req.params;
|
||||
|
||||
if (!config.nocodb.shiftsSheetId || !config.nocodb.shiftSignupsSheetId) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Shifts not properly configured'
|
||||
});
|
||||
}
|
||||
|
||||
// Find the signup by user ID (signup record ID)
|
||||
const signup = await nocodbService.getById(config.nocodb.shiftSignupsSheetId, userId);
|
||||
|
||||
if (!signup) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Signup not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the signup belongs to the specified shift
|
||||
if (signup['Shift ID'] !== parseInt(shiftId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Signup does not belong to this shift'
|
||||
});
|
||||
}
|
||||
|
||||
// Update signup status to cancelled
|
||||
await nocodbService.update(config.nocodb.shiftSignupsSheetId, userId, {
|
||||
'Status': 'Cancelled'
|
||||
});
|
||||
|
||||
// Update shift volunteer count
|
||||
const allSignups = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId);
|
||||
const confirmedSignups = (allSignups.list || []).filter(s =>
|
||||
s['Shift ID'] === parseInt(shiftId) && s.Status === 'Confirmed'
|
||||
);
|
||||
const newCount = confirmedSignups.length;
|
||||
|
||||
const shift = await nocodbService.getById(config.nocodb.shiftsSheetId, shiftId);
|
||||
await nocodbService.update(config.nocodb.shiftsSheetId, shiftId, {
|
||||
'Current Volunteers': newCount,
|
||||
'Status': newCount >= shift['Max Volunteers'] ? 'Full' : 'Open'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'User successfully removed from shift'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error removing user from shift:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to remove user from shift'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Admin: Email shift details to all volunteers
|
||||
async emailShiftDetails(req, res) {
|
||||
try {
|
||||
const { shiftId } = req.params;
|
||||
|
||||
if (!config.nocodb.shiftsSheetId || !config.nocodb.shiftSignupsSheetId) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Shifts not properly configured'
|
||||
});
|
||||
}
|
||||
|
||||
// Get shift details
|
||||
const shift = await nocodbService.getById(config.nocodb.shiftsSheetId, shiftId);
|
||||
if (!shift) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Shift not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get all confirmed signups for this shift
|
||||
const allSignups = await nocodbService.getAll(config.nocodb.shiftSignupsSheetId);
|
||||
const shiftSignups = (allSignups.list || []).filter(signup =>
|
||||
signup['Shift ID'] === parseInt(shiftId) && signup.Status === 'Confirmed'
|
||||
);
|
||||
|
||||
if (shiftSignups.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No volunteers signed up for this shift'
|
||||
});
|
||||
}
|
||||
|
||||
// Import email service
|
||||
const { sendEmail } = require('../services/email');
|
||||
const emailTemplates = require('../services/emailTemplates');
|
||||
const config_app = require('../config');
|
||||
|
||||
// Prepare email template variables
|
||||
const shiftDate = new Date(shift.Date);
|
||||
const baseUrl = config_app.isProduction ?
|
||||
`https://map.${config_app.domain}` :
|
||||
`http://localhost:${config_app.port}`;
|
||||
|
||||
const hasDescription = shift.Description && shift.Description.trim().length > 0;
|
||||
|
||||
const templateVariables = {
|
||||
APP_NAME: 'Volunteer Shift Manager',
|
||||
SHIFT_TITLE: shift.Title,
|
||||
SHIFT_DATE: shiftDate.toLocaleDateString(),
|
||||
SHIFT_START_TIME: shift['Start Time'],
|
||||
SHIFT_END_TIME: shift['End Time'],
|
||||
SHIFT_LOCATION: shift.Location || 'TBD',
|
||||
CURRENT_VOLUNTEERS: shiftSignups.length,
|
||||
MAX_VOLUNTEERS: shift['Max Volunteers'],
|
||||
SHIFT_STATUS: shift.Status || 'Open',
|
||||
SHIFT_STATUS_CLASS: (shift.Status || 'Open').toLowerCase(),
|
||||
SHIFT_DESCRIPTION: shift.Description || '',
|
||||
SHIFT_DESCRIPTION_SECTION: hasDescription ? `ADDITIONAL INFORMATION:\n======================\n${shift.Description}` : '',
|
||||
DESCRIPTION_DISPLAY: hasDescription ? 'block' : 'none',
|
||||
TIMESTAMP: new Date().toLocaleString()
|
||||
};
|
||||
|
||||
// Send emails to all volunteers
|
||||
const emailResults = [];
|
||||
const failedEmails = [];
|
||||
|
||||
for (const signup of shiftSignups) {
|
||||
try {
|
||||
const userVariables = {
|
||||
...templateVariables,
|
||||
USER_NAME: signup['User Name'] || signup['User Email'],
|
||||
USER_EMAIL: signup['User Email']
|
||||
};
|
||||
|
||||
const emailContent = await emailTemplates.render('shift-details', userVariables);
|
||||
|
||||
await sendEmail({
|
||||
to: signup['User Email'],
|
||||
subject: `Shift Details: ${shift.Title} - ${shiftDate.toLocaleDateString()}`,
|
||||
text: emailContent.text,
|
||||
html: emailContent.html
|
||||
});
|
||||
|
||||
emailResults.push({
|
||||
email: signup['User Email'],
|
||||
name: signup['User Name'],
|
||||
success: true
|
||||
});
|
||||
|
||||
logger.info(`Sent shift details email to: ${signup['User Email']}`);
|
||||
} catch (emailError) {
|
||||
logger.error(`Failed to send shift details email to ${signup['User Email']}:`, emailError);
|
||||
failedEmails.push({
|
||||
email: signup['User Email'],
|
||||
name: signup['User Name'],
|
||||
error: emailError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = emailResults.length;
|
||||
const failCount = failedEmails.length;
|
||||
|
||||
if (successCount === 0) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to send any emails',
|
||||
details: failedEmails
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Sent shift details to ${successCount} volunteer${successCount !== 1 ? 's' : ''}${failCount > 0 ? `, ${failCount} failed` : ''}`,
|
||||
results: {
|
||||
successful: emailResults,
|
||||
failed: failedEmails,
|
||||
shift: {
|
||||
id: shiftId,
|
||||
title: shift.Title,
|
||||
date: shiftDate.toLocaleDateString(),
|
||||
volunteers: shiftSignups.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error sending shift details emails:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to send shift details emails'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ShiftsController();
|
||||
@@ -211,6 +211,127 @@ class UsersController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async emailAllUsers(req, res) {
|
||||
try {
|
||||
const { subject, content } = req.body;
|
||||
|
||||
if (!subject || !content) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Subject and content are required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.nocodb.loginSheetId) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Login sheet not configured'
|
||||
});
|
||||
}
|
||||
|
||||
// Get all users
|
||||
const response = await nocodbService.getAll(config.nocodb.loginSheetId, {
|
||||
limit: 1000
|
||||
});
|
||||
|
||||
const users = response.list || [];
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No users found to email'
|
||||
});
|
||||
}
|
||||
|
||||
// Import email service
|
||||
const { sendEmail } = require('../services/email');
|
||||
const emailTemplates = require('../services/emailTemplates');
|
||||
const config_app = require('../config');
|
||||
|
||||
// Convert rich text content to plain text for the text version
|
||||
const stripHtmlTags = (html) => {
|
||||
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
||||
};
|
||||
|
||||
// Prepare base template variables
|
||||
const baseTemplateVariables = {
|
||||
APP_NAME: 'CMlite Map - User Broadcast',
|
||||
EMAIL_SUBJECT: subject,
|
||||
EMAIL_CONTENT: content,
|
||||
EMAIL_CONTENT_TEXT: stripHtmlTags(content),
|
||||
SENDER_NAME: req.session.userName || req.session.userEmail || 'Administrator',
|
||||
TIMESTAMP: new Date().toLocaleString()
|
||||
};
|
||||
|
||||
// Send emails to all users
|
||||
const emailResults = [];
|
||||
const failedEmails = [];
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
const userVariables = {
|
||||
...baseTemplateVariables,
|
||||
USER_NAME: user.Name || user.name || user.Email || user.email || 'User',
|
||||
USER_EMAIL: user.Email || user.email
|
||||
};
|
||||
|
||||
const emailContent = await emailTemplates.render('user-broadcast', userVariables);
|
||||
|
||||
await sendEmail({
|
||||
to: user.Email || user.email,
|
||||
subject: subject,
|
||||
text: emailContent.text,
|
||||
html: emailContent.html
|
||||
});
|
||||
|
||||
emailResults.push({
|
||||
email: user.Email || user.email,
|
||||
name: user.Name || user.name || user.Email || user.email,
|
||||
success: true
|
||||
});
|
||||
|
||||
logger.info(`Sent broadcast email to: ${user.Email || user.email}`);
|
||||
} catch (emailError) {
|
||||
logger.error(`Failed to send broadcast email to ${user.Email || user.email}:`, emailError);
|
||||
failedEmails.push({
|
||||
email: user.Email || user.email,
|
||||
name: user.Name || user.name || user.Email || user.email,
|
||||
error: emailError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = emailResults.length;
|
||||
const failCount = failedEmails.length;
|
||||
|
||||
if (successCount === 0) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to send any emails',
|
||||
details: failedEmails
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Sent email to ${successCount} user${successCount !== 1 ? 's' : ''}${failCount > 0 ? `, ${failCount} failed` : ''}`,
|
||||
results: {
|
||||
successful: emailResults,
|
||||
failed: failedEmails,
|
||||
total: users.length,
|
||||
subject: subject
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error sending broadcast email:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to send broadcast email'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new UsersController();
|
||||
@@ -403,6 +403,86 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shift User Management Modal -->
|
||||
<div id="shift-user-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Manage Volunteers</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="close-user-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="shift-info">
|
||||
<h4 id="modal-shift-title"></h4>
|
||||
<p id="modal-shift-details"></p>
|
||||
</div>
|
||||
|
||||
<div class="add-user-section">
|
||||
<h4>Add Volunteer</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="user-select">Select User:</label>
|
||||
<select id="user-select" style="width: 100%;">
|
||||
<option value="">Select a user...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" class="btn btn-primary" id="add-user-btn">Add User</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="current-volunteers-section">
|
||||
<h4>Current Volunteers</h4>
|
||||
<div class="volunteer-actions-header">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="email-shift-details-btn">
|
||||
📧 Email Shift Details
|
||||
</button>
|
||||
</div>
|
||||
<div id="current-volunteers-list">
|
||||
<!-- Current volunteers will be loaded here -->
|
||||
</div>
|
||||
|
||||
<!-- Shift Email Progress Container -->
|
||||
<div id="shift-email-progress-container" class="email-progress-container">
|
||||
<div class="progress-header">
|
||||
<div class="progress-title">Sending Shift Details...</div>
|
||||
<div class="progress-stats">
|
||||
<div class="progress-stat success">
|
||||
<span>✓</span>
|
||||
<span id="shift-success-count">0</span> sent
|
||||
</div>
|
||||
<div class="progress-stat error">
|
||||
<span>✗</span>
|
||||
<span id="shift-error-count">0</span> failed
|
||||
</div>
|
||||
<div class="progress-stat pending">
|
||||
<span>⏳</span>
|
||||
<span id="shift-pending-count">0</span> pending
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="shift-email-progress-bar">
|
||||
<div class="progress-text" id="shift-progress-text">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-status-list" id="shift-email-status-list">
|
||||
<!-- Status items will be added dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="progress-actions">
|
||||
<button type="button" class="progress-close-btn" id="close-shift-progress-btn" style="display: none;">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Users Section -->
|
||||
@@ -453,10 +533,136 @@
|
||||
</div>
|
||||
|
||||
<div class="users-list">
|
||||
<div class="users-list-header">
|
||||
<h3>All Users</h3>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="email-all-users-btn">
|
||||
📧 Email All Users
|
||||
</button>
|
||||
</div>
|
||||
<!-- User table will be dynamically inserted here -->
|
||||
<p id="users-loading" class="loading-message">Loading users...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email All Users Modal -->
|
||||
<div id="email-users-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content modal-large">
|
||||
<div class="modal-header">
|
||||
<h3>Email All Users</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="close-email-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="email-form">
|
||||
<form id="email-users-form">
|
||||
<div class="form-group">
|
||||
<label for="email-subject">Subject</label>
|
||||
<input type="text" id="email-subject" required placeholder="Enter email subject">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email-content">Message</label>
|
||||
<div class="rich-text-toolbar">
|
||||
<button type="button" class="toolbar-btn" data-command="bold" title="Bold">
|
||||
<strong>B</strong>
|
||||
</button>
|
||||
<button type="button" class="toolbar-btn" data-command="italic" title="Italic">
|
||||
<em>I</em>
|
||||
</button>
|
||||
<button type="button" class="toolbar-btn" data-command="underline" title="Underline">
|
||||
<u>U</u>
|
||||
</button>
|
||||
<button type="button" class="toolbar-btn" data-command="insertUnorderedList" title="Bullet List">
|
||||
• List
|
||||
</button>
|
||||
<button type="button" class="toolbar-btn" data-command="insertOrderedList" title="Numbered List">
|
||||
1. List
|
||||
</button>
|
||||
<button type="button" class="toolbar-btn" data-command="createLink" title="Insert Link">
|
||||
🔗 Link
|
||||
</button>
|
||||
</div>
|
||||
<div id="email-content" class="rich-text-editor" contenteditable="true"
|
||||
placeholder="Type your message here..."
|
||||
style="min-height: 200px; border: 1px solid #ddd; padding: 15px; border-radius: 4px; background: white;">
|
||||
</div>
|
||||
<small class="help-text">Use the toolbar above to format your message</small>
|
||||
</div>
|
||||
|
||||
<div class="email-preview-section">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="show-preview" />
|
||||
Show email preview
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="email-preview" class="email-preview" style="display: none;">
|
||||
<h4>Email Preview:</h4>
|
||||
<div class="email-preview-content">
|
||||
<div class="preview-header">
|
||||
<strong>Subject:</strong> <span id="preview-subject">Your subject will appear here</span>
|
||||
</div>
|
||||
<div class="preview-body" id="preview-body">
|
||||
Your message will appear here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-recipients-info">
|
||||
<p><strong>Recipients:</strong> <span id="recipients-count">Loading...</span> users will receive this email</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="send-email-btn">
|
||||
📧 Send Email to All Users
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-email-btn">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Email Progress Container -->
|
||||
<div id="email-progress-container" class="email-progress-container">
|
||||
<div class="progress-header">
|
||||
<div class="progress-title">Sending Emails...</div>
|
||||
<div class="progress-stats">
|
||||
<div class="progress-stat success">
|
||||
<span>✓</span>
|
||||
<span id="success-count">0</span> sent
|
||||
</div>
|
||||
<div class="progress-stat error">
|
||||
<span>✗</span>
|
||||
<span id="error-count">0</span> failed
|
||||
</div>
|
||||
<div class="progress-stat pending">
|
||||
<span>⏳</span>
|
||||
<span id="pending-count">0</span> pending
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="email-progress-bar">
|
||||
<div class="progress-text" id="progress-text">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-status-list" id="email-status-list">
|
||||
<!-- Status items will be added dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="progress-actions">
|
||||
<button type="button" class="progress-close-btn" id="close-progress-btn" style="display: none;">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Map Cuts Section -->
|
||||
|
||||
@@ -2539,3 +2539,472 @@
|
||||
border-color: #ddd;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Shift User Management Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 25px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.shift-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.shift-info h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.shift-info p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.add-user-section {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.add-user-section h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.current-volunteers-section h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.volunteer-actions-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Users list header styling */
|
||||
.users-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
.users-list-header h3 {
|
||||
margin: 0;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
/* Email modal styling */
|
||||
.modal-large {
|
||||
max-width: 800px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.email-form {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.rich-text-toolbar {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.toolbar-btn:active, .toolbar-btn.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.rich-text-editor {
|
||||
min-height: 200px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
background: white;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.rich-text-editor:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(160, 44, 141, 0.2);
|
||||
}
|
||||
|
||||
.rich-text-editor:empty:before {
|
||||
content: attr(placeholder);
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.email-preview {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.email-preview-content {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.email-recipients-info {
|
||||
background: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
.email-recipients-info p {
|
||||
margin: 0;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.volunteer-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.volunteer-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.volunteer-name {
|
||||
font-weight: bold;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.volunteer-email {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.volunteer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.no-volunteers {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* Enhance existing shift admin items */
|
||||
.shift-admin-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.manage-volunteers-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* User select dropdown */
|
||||
#user-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#user-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
/* Responsive modal */
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.volunteer-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.volunteer-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Email Progress Indicators */
|
||||
.email-progress-container {
|
||||
display: none;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.email-progress-container.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.progress-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.progress-stat.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.progress-stat.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.progress-stat.pending {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
background-color: #e9ecef;
|
||||
border-radius: 10px;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color) 0%, #28a745 100%);
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar.complete {
|
||||
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
|
||||
}
|
||||
|
||||
.progress-bar.error {
|
||||
background: linear-gradient(90deg, #dc3545 0%, #e74c3c 100%);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.email-status-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.email-status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 15px;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.email-status-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.email-status-recipient {
|
||||
font-weight: 500;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.email-status-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.email-status-result.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.email-status-result.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.email-status-result.pending {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.progress-spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #6c757d;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.progress-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.progress-close-btn {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.progress-close-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
@@ -500,6 +500,12 @@ function showSection(sectionId) {
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Special handling for shifts section
|
||||
if (sectionId === 'shifts') {
|
||||
console.log('Loading shifts for admin panel...');
|
||||
loadAdminShifts();
|
||||
}
|
||||
}
|
||||
|
||||
// Update map from input fields
|
||||
@@ -1113,17 +1119,31 @@ function debounce(func, wait) {
|
||||
|
||||
// Add shift management functions
|
||||
async function loadAdminShifts() {
|
||||
const list = document.getElementById('admin-shifts-list');
|
||||
if (list) {
|
||||
list.innerHTML = '<p>Loading shifts...</p>';
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Loading admin shifts...');
|
||||
const response = await fetch('/api/shifts/admin');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
console.log('Successfully loaded', data.shifts.length, 'shifts');
|
||||
displayAdminShifts(data.shifts);
|
||||
} else {
|
||||
console.error('Failed to load shifts:', data.error);
|
||||
if (list) {
|
||||
list.innerHTML = '<p>Failed to load shifts</p>';
|
||||
}
|
||||
showStatus('Failed to load shifts', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading admin shifts:', error);
|
||||
if (list) {
|
||||
list.innerHTML = '<p>Error loading shifts</p>';
|
||||
}
|
||||
showStatus('Failed to load shifts', 'error');
|
||||
}
|
||||
}
|
||||
@@ -1145,6 +1165,8 @@ function displayAdminShifts(shifts) {
|
||||
const shiftDate = new Date(shift.Date);
|
||||
const signupCount = shift.signups ? shift.signups.length : 0;
|
||||
|
||||
console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []);
|
||||
|
||||
return `
|
||||
<div class="shift-admin-item">
|
||||
<div>
|
||||
@@ -1155,6 +1177,7 @@ function displayAdminShifts(shifts) {
|
||||
<p class="status-${(shift.Status || 'open').toLowerCase()}">${shift.Status || 'Open'}</p>
|
||||
</div>
|
||||
<div class="shift-actions">
|
||||
<button class="btn btn-primary btn-sm manage-volunteers-btn" data-shift-id="${shift.ID}" data-shift='${JSON.stringify(shift).replace(/'/g, "'")}'>Manage Volunteers</button>
|
||||
<button class="btn btn-secondary btn-sm edit-shift-btn" data-shift-id="${shift.ID}">Edit</button>
|
||||
<button class="btn btn-danger btn-sm delete-shift-btn" data-shift-id="${shift.ID}">Delete</button>
|
||||
</div>
|
||||
@@ -1187,6 +1210,11 @@ function setupShiftActionListeners() {
|
||||
const shiftId = e.target.getAttribute('data-shift-id');
|
||||
console.log('Edit button clicked for shift:', shiftId);
|
||||
editShift(shiftId);
|
||||
} else if (e.target.classList.contains('manage-volunteers-btn')) {
|
||||
const shiftId = e.target.getAttribute('data-shift-id');
|
||||
const shiftData = JSON.parse(e.target.getAttribute('data-shift').replace(/'/g, "'"));
|
||||
console.log('Manage volunteers clicked for shift:', shiftId);
|
||||
showShiftUserModal(shiftId, shiftData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1207,6 +1235,7 @@ async function deleteShift(shiftId) {
|
||||
if (data.success) {
|
||||
showStatus('Shift deleted successfully', 'success');
|
||||
await loadAdminShifts();
|
||||
console.log('Refreshed shifts list after deleting shift');
|
||||
} else {
|
||||
showStatus(data.error || 'Failed to delete shift', 'error');
|
||||
}
|
||||
@@ -1250,6 +1279,7 @@ async function createShift(e) {
|
||||
showStatus('Shift created successfully', 'success');
|
||||
document.getElementById('shift-form').reset();
|
||||
await loadAdminShifts();
|
||||
console.log('Refreshed shifts list after creating new shift');
|
||||
} else {
|
||||
showStatus(data.error || 'Failed to create shift', 'error');
|
||||
}
|
||||
@@ -1304,13 +1334,29 @@ function displayUsers(users) {
|
||||
const container = document.querySelector('.users-list');
|
||||
if (!container) return;
|
||||
|
||||
// Find or create the users table container, preserving the header
|
||||
let usersTableContainer = container.querySelector('.users-table-container');
|
||||
if (!usersTableContainer) {
|
||||
// If container doesn't exist, create it after the header
|
||||
const header = container.querySelector('.users-list-header');
|
||||
usersTableContainer = document.createElement('div');
|
||||
usersTableContainer.className = 'users-table-container';
|
||||
|
||||
if (header && header.nextSibling) {
|
||||
container.insertBefore(usersTableContainer, header.nextSibling);
|
||||
} else if (header) {
|
||||
container.appendChild(usersTableContainer);
|
||||
} else {
|
||||
container.appendChild(usersTableContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
container.innerHTML = '<h3>Existing Users</h3><p class="empty-message">No users found.</p>';
|
||||
usersTableContainer.innerHTML = '<p class="empty-message">No users found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tableHtml = `
|
||||
<h3>Existing Users</h3>
|
||||
<div class="users-table-wrapper">
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
@@ -1322,7 +1368,7 @@ function displayUsers(users) {
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="users-table-body">
|
||||
${users.map(user => {
|
||||
const createdDate = user.created_at || user['Created At'] || user.createdAt;
|
||||
const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
|
||||
@@ -1376,7 +1422,7 @@ function displayUsers(users) {
|
||||
<p id="users-loading" class="loading-message" style="display: none;">Loading...</p>
|
||||
`;
|
||||
|
||||
container.innerHTML = tableHtml;
|
||||
usersTableContainer.innerHTML = tableHtml;
|
||||
setupUserActionListeners();
|
||||
}
|
||||
|
||||
@@ -1402,6 +1448,9 @@ function setupUserActionListeners() {
|
||||
const userEmail = e.target.getAttribute('data-user-email');
|
||||
console.log('Send login details button clicked for user:', userId);
|
||||
sendLoginDetailsToUser(userId, userEmail);
|
||||
} else if (e.target.id === 'email-all-users-btn') {
|
||||
console.log('Email All Users button clicked');
|
||||
showEmailUsersModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1545,6 +1594,318 @@ function clearUserForm() {
|
||||
}
|
||||
}
|
||||
|
||||
// Email All Users Functions
|
||||
let allUsersData = [];
|
||||
|
||||
async function showEmailUsersModal() {
|
||||
// Load current users data
|
||||
try {
|
||||
const response = await fetch('/api/users');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.users) {
|
||||
allUsersData = data.users;
|
||||
|
||||
// Update recipients count
|
||||
const recipientsCount = document.getElementById('recipients-count');
|
||||
if (recipientsCount) {
|
||||
recipientsCount.textContent = `${allUsersData.length}`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users for email:', error);
|
||||
showStatus('Failed to load user data', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show modal
|
||||
const modal = document.getElementById('email-users-modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Clear previous content
|
||||
document.getElementById('email-subject').value = '';
|
||||
document.getElementById('email-content').innerHTML = '';
|
||||
document.getElementById('show-preview').checked = false;
|
||||
document.getElementById('email-preview').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function closeEmailUsersModal() {
|
||||
const modal = document.getElementById('email-users-modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setupRichTextEditor() {
|
||||
const toolbar = document.querySelector('.rich-text-toolbar');
|
||||
const editor = document.getElementById('email-content');
|
||||
|
||||
if (!toolbar || !editor) return;
|
||||
|
||||
// Handle toolbar button clicks
|
||||
toolbar.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('toolbar-btn')) {
|
||||
e.preventDefault();
|
||||
const command = e.target.getAttribute('data-command');
|
||||
|
||||
if (command === 'createLink') {
|
||||
const url = prompt('Enter the URL:');
|
||||
if (url) {
|
||||
document.execCommand(command, false, url);
|
||||
}
|
||||
} else {
|
||||
document.execCommand(command, false, null);
|
||||
}
|
||||
|
||||
// Update preview if visible
|
||||
updateEmailPreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview on content change
|
||||
editor.addEventListener('input', updateEmailPreview);
|
||||
|
||||
// Handle preview toggle
|
||||
const showPreviewCheckbox = document.getElementById('show-preview');
|
||||
if (showPreviewCheckbox) {
|
||||
showPreviewCheckbox.addEventListener('change', togglePreview);
|
||||
}
|
||||
|
||||
// Update preview when subject changes
|
||||
const subjectInput = document.getElementById('email-subject');
|
||||
if (subjectInput) {
|
||||
subjectInput.addEventListener('input', updateEmailPreview);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePreview() {
|
||||
const preview = document.getElementById('email-preview');
|
||||
const checkbox = document.getElementById('show-preview');
|
||||
|
||||
if (preview && checkbox) {
|
||||
if (checkbox.checked) {
|
||||
preview.style.display = 'block';
|
||||
updateEmailPreview();
|
||||
} else {
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateEmailPreview() {
|
||||
const previewSubject = document.getElementById('preview-subject');
|
||||
const previewBody = document.getElementById('preview-body');
|
||||
const subjectInput = document.getElementById('email-subject');
|
||||
const contentEditor = document.getElementById('email-content');
|
||||
|
||||
if (previewSubject && subjectInput) {
|
||||
previewSubject.textContent = subjectInput.value || 'Your subject will appear here';
|
||||
}
|
||||
|
||||
if (previewBody && contentEditor) {
|
||||
const content = contentEditor.innerHTML || 'Your message will appear here';
|
||||
previewBody.innerHTML = content;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEmailToAllUsers(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const subject = document.getElementById('email-subject').value.trim();
|
||||
const content = document.getElementById('email-content').innerHTML.trim();
|
||||
|
||||
if (!subject) {
|
||||
showStatus('Please enter an email subject', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content || content === '<br>' || content === '') {
|
||||
showStatus('Please enter email content', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (allUsersData.length === 0) {
|
||||
showStatus('No users found to email', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMessage = `Send this email to all ${allUsersData.length} users?`;
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize progress tracking
|
||||
initializeEmailProgress(allUsersData.length);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users/email-all', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: subject,
|
||||
content: content
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Display detailed results
|
||||
updateEmailProgress(data.results);
|
||||
showStatus(data.message, 'success');
|
||||
console.log('Email results:', data.results);
|
||||
} else {
|
||||
showEmailError(data.error || 'Failed to send emails');
|
||||
if (data.details) {
|
||||
console.error('Failed email details:', data.details);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending emails to all users:', error);
|
||||
showEmailError('Failed to send emails - Network error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize email progress display
|
||||
function initializeEmailProgress(totalCount) {
|
||||
const progressContainer = document.getElementById('email-progress-container');
|
||||
const statusList = document.getElementById('email-status-list');
|
||||
const pendingCountEl = document.getElementById('pending-count');
|
||||
const successCountEl = document.getElementById('success-count');
|
||||
const errorCountEl = document.getElementById('error-count');
|
||||
const progressBar = document.getElementById('email-progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const closeBtn = document.getElementById('close-progress-btn');
|
||||
|
||||
// Show progress container
|
||||
progressContainer.classList.add('show');
|
||||
|
||||
// Reset counters
|
||||
pendingCountEl.textContent = totalCount;
|
||||
successCountEl.textContent = '0';
|
||||
errorCountEl.textContent = '0';
|
||||
|
||||
// Reset progress bar
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.classList.remove('complete', 'error');
|
||||
progressText.textContent = '0%';
|
||||
|
||||
// Clear status list
|
||||
statusList.innerHTML = '';
|
||||
|
||||
// Hide close button initially
|
||||
closeBtn.style.display = 'none';
|
||||
|
||||
// Add status items for each user
|
||||
allUsersData.forEach(user => {
|
||||
const statusItem = document.createElement('div');
|
||||
statusItem.className = 'email-status-item';
|
||||
statusItem.innerHTML = `
|
||||
<div class="email-status-recipient">${user.Name || user.Email}</div>
|
||||
<div class="email-status-result pending">
|
||||
<div class="progress-spinner"></div>
|
||||
<span>Sending...</span>
|
||||
</div>
|
||||
`;
|
||||
statusList.appendChild(statusItem);
|
||||
});
|
||||
}
|
||||
|
||||
// Update progress with results
|
||||
function updateEmailProgress(results) {
|
||||
const statusList = document.getElementById('email-status-list');
|
||||
const pendingCountEl = document.getElementById('pending-count');
|
||||
const successCountEl = document.getElementById('success-count');
|
||||
const errorCountEl = document.getElementById('error-count');
|
||||
const progressBar = document.getElementById('email-progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const closeBtn = document.getElementById('close-progress-btn');
|
||||
|
||||
const successful = results.successful || [];
|
||||
const failed = results.failed || [];
|
||||
const total = results.total || (successful.length + failed.length);
|
||||
|
||||
// Update counters
|
||||
successCountEl.textContent = successful.length;
|
||||
errorCountEl.textContent = failed.length;
|
||||
pendingCountEl.textContent = '0';
|
||||
|
||||
// Update progress bar
|
||||
const percentage = ((successful.length + failed.length) / total * 100).toFixed(1);
|
||||
progressBar.style.width = percentage + '%';
|
||||
progressText.textContent = percentage + '%';
|
||||
|
||||
if (failed.length > 0) {
|
||||
progressBar.classList.add('error');
|
||||
} else {
|
||||
progressBar.classList.add('complete');
|
||||
}
|
||||
|
||||
// Update individual status items
|
||||
const statusItems = statusList.children;
|
||||
|
||||
// Update successful emails
|
||||
successful.forEach(result => {
|
||||
const statusItem = Array.from(statusItems).find(item =>
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
|
||||
);
|
||||
if (statusItem) {
|
||||
statusItem.querySelector('.email-status-result').innerHTML = `
|
||||
<span class="email-status-result success">✓ Sent</span>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Update failed emails
|
||||
failed.forEach(result => {
|
||||
const statusItem = Array.from(statusItems).find(item =>
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
|
||||
);
|
||||
if (statusItem) {
|
||||
statusItem.querySelector('.email-status-result').innerHTML = `
|
||||
<span class="email-status-result error" title="${result.error || 'Unknown error'}">✗ Failed</span>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Show close button
|
||||
closeBtn.style.display = 'block';
|
||||
closeBtn.onclick = () => {
|
||||
document.getElementById('email-progress-container').classList.remove('show');
|
||||
closeEmailUsersModal();
|
||||
};
|
||||
}
|
||||
|
||||
// Show email error
|
||||
function showEmailError(message) {
|
||||
const progressContainer = document.getElementById('email-progress-container');
|
||||
const progressBar = document.getElementById('email-progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const closeBtn = document.getElementById('close-progress-btn');
|
||||
|
||||
// Show progress container if not visible
|
||||
progressContainer.classList.add('show');
|
||||
|
||||
// Update progress bar to show error
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.classList.add('error');
|
||||
progressText.textContent = 'Error';
|
||||
|
||||
// Show close button
|
||||
closeBtn.style.display = 'block';
|
||||
closeBtn.onclick = () => {
|
||||
progressContainer.classList.remove('show');
|
||||
};
|
||||
|
||||
showStatus(message, 'error');
|
||||
}
|
||||
|
||||
// Initialize NocoDB links in admin panel
|
||||
async function initializeNocodbLinks() {
|
||||
console.log('Starting NocoDB links initialization...');
|
||||
@@ -1626,3 +1987,470 @@ function setAdminNocodbLink(elementId, url) {
|
||||
console.error(`✗ Element not found: ${elementId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Shift User Management Functions
|
||||
let currentShiftData = null;
|
||||
let allUsers = [];
|
||||
|
||||
// Load all users for the dropdown
|
||||
async function loadAllUsers() {
|
||||
try {
|
||||
const response = await fetch('/api/users');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
allUsers = data.users;
|
||||
populateUserSelect();
|
||||
} else {
|
||||
console.error('Failed to load users:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate user select dropdown
|
||||
function populateUserSelect() {
|
||||
const select = document.getElementById('user-select');
|
||||
if (!select) return;
|
||||
|
||||
// Clear existing options except the first one
|
||||
select.innerHTML = '<option value="">Select a user...</option>';
|
||||
|
||||
allUsers.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.email || user.Email;
|
||||
option.textContent = `${user.name || user.Name || ''} (${user.email || user.Email})`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Show the shift user management modal
|
||||
async function showShiftUserModal(shiftId, shiftData) {
|
||||
currentShiftData = { ...shiftData, ID: shiftId };
|
||||
|
||||
// Update modal title and info
|
||||
document.getElementById('modal-shift-title').textContent = shiftData.Title;
|
||||
const shiftDate = new Date(shiftData.Date);
|
||||
document.getElementById('modal-shift-details').textContent =
|
||||
`${shiftDate.toLocaleDateString()} | ${shiftData['Start Time']} - ${shiftData['End Time']} | ${shiftData.Location || 'TBD'}`;
|
||||
|
||||
// Load users if not already loaded
|
||||
if (allUsers.length === 0) {
|
||||
await loadAllUsers();
|
||||
}
|
||||
|
||||
// Display current volunteers
|
||||
displayCurrentVolunteers(shiftData.signups || []);
|
||||
|
||||
// Show modal
|
||||
document.getElementById('shift-user-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Display current volunteers in the modal
|
||||
function displayCurrentVolunteers(volunteers) {
|
||||
const container = document.getElementById('current-volunteers-list');
|
||||
|
||||
if (!volunteers || volunteers.length === 0) {
|
||||
container.innerHTML = '<div class="no-volunteers">No volunteers signed up yet.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = volunteers.map(volunteer => `
|
||||
<div class="volunteer-item">
|
||||
<div class="volunteer-info">
|
||||
<div class="volunteer-name">${escapeHtml(volunteer['User Name'] || volunteer['User Email'] || 'Unknown')}</div>
|
||||
<div class="volunteer-email">${escapeHtml(volunteer['User Email'])}</div>
|
||||
</div>
|
||||
<div class="volunteer-actions">
|
||||
<button class="btn btn-danger btn-sm remove-volunteer-btn"
|
||||
data-volunteer-id="${volunteer.ID || volunteer.id}"
|
||||
data-volunteer-email="${volunteer['User Email']}">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners for remove buttons
|
||||
setupVolunteerActionListeners();
|
||||
}
|
||||
|
||||
// Setup event listeners for volunteer actions
|
||||
function setupVolunteerActionListeners() {
|
||||
const container = document.getElementById('current-volunteers-list');
|
||||
|
||||
container.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-volunteer-btn')) {
|
||||
const volunteerId = e.target.getAttribute('data-volunteer-id');
|
||||
const volunteerEmail = e.target.getAttribute('data-volunteer-email');
|
||||
removeVolunteerFromShift(volunteerId, volunteerEmail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add user to shift
|
||||
async function addUserToShift() {
|
||||
const userSelect = document.getElementById('user-select');
|
||||
const userEmail = userSelect.value;
|
||||
|
||||
if (!userEmail) {
|
||||
showStatus('Please select a user to add', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentShiftData) {
|
||||
showStatus('No shift selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/add-user`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ userEmail })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showStatus('User successfully added to shift', 'success');
|
||||
userSelect.value = ''; // Clear selection
|
||||
|
||||
// Refresh the shift data and reload volunteers
|
||||
await refreshCurrentShiftData();
|
||||
console.log('Refreshed shift data after adding user');
|
||||
} else {
|
||||
showStatus(data.error || 'Failed to add user to shift', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding user to shift:', error);
|
||||
showStatus('Failed to add user to shift', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove volunteer from shift
|
||||
async function removeVolunteerFromShift(volunteerId, volunteerEmail) {
|
||||
if (!confirm(`Are you sure you want to remove ${volunteerEmail} from this shift?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentShiftData) {
|
||||
showStatus('No shift selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/remove-user/${volunteerId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showStatus('Volunteer successfully removed from shift', 'success');
|
||||
|
||||
// Refresh the shift data and reload volunteers
|
||||
await refreshCurrentShiftData();
|
||||
console.log('Refreshed shift data after removing volunteer');
|
||||
} else {
|
||||
showStatus(data.error || 'Failed to remove volunteer from shift', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing volunteer from shift:', error);
|
||||
showStatus('Failed to remove volunteer from shift', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh current shift data
|
||||
async function refreshCurrentShiftData() {
|
||||
if (!currentShiftData) return;
|
||||
|
||||
try {
|
||||
console.log('Refreshing shift data for shift ID:', currentShiftData.ID);
|
||||
|
||||
// Reload admin shifts to get updated data
|
||||
const response = await fetch('/api/shifts/admin');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const updatedShift = data.shifts.find(s => s.ID === currentShiftData.ID);
|
||||
if (updatedShift) {
|
||||
console.log('Found updated shift with', updatedShift.signups?.length || 0, 'volunteers');
|
||||
currentShiftData = updatedShift;
|
||||
displayCurrentVolunteers(updatedShift.signups || []);
|
||||
|
||||
// Immediately refresh the main shifts list to show updated counts
|
||||
console.log('Refreshing main shifts list with', data.shifts.length, 'shifts');
|
||||
displayAdminShifts(data.shifts);
|
||||
} else {
|
||||
console.warn('Could not find updated shift with ID:', currentShiftData.ID);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to refresh shift data:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing shift data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeShiftUserModal() {
|
||||
document.getElementById('shift-user-modal').style.display = 'none';
|
||||
currentShiftData = null;
|
||||
|
||||
// Refresh the main shifts list one more time when closing the modal
|
||||
// to ensure any changes are reflected
|
||||
console.log('Refreshing shifts list on modal close');
|
||||
loadAdminShifts();
|
||||
}
|
||||
|
||||
// Email shift details to all volunteers
|
||||
async function emailShiftDetails() {
|
||||
if (!currentShiftData) {
|
||||
showStatus('No shift selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are volunteers to email
|
||||
const volunteers = currentShiftData.signups || [];
|
||||
if (volunteers.length === 0) {
|
||||
showStatus('No volunteers signed up for this shift', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm action
|
||||
const confirmMessage = `Send shift details email to ${volunteers.length} volunteer${volunteers.length !== 1 ? 's' : ''}?`;
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize progress tracking for shift emails
|
||||
initializeShiftEmailProgress(volunteers.length);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/email-details`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Display detailed results
|
||||
updateShiftEmailProgress(data.results);
|
||||
showStatus(data.message, 'success');
|
||||
console.log('Email results:', data.results);
|
||||
} else {
|
||||
showShiftEmailError(data.error || 'Failed to send emails');
|
||||
if (data.details) {
|
||||
console.error('Failed email details:', data.details);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending shift details emails:', error);
|
||||
showShiftEmailError('Failed to send emails - Network error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize shift email progress display
|
||||
function initializeShiftEmailProgress(totalCount) {
|
||||
const progressContainer = document.getElementById('shift-email-progress-container');
|
||||
const statusList = document.getElementById('shift-email-status-list');
|
||||
const pendingCountEl = document.getElementById('shift-pending-count');
|
||||
const successCountEl = document.getElementById('shift-success-count');
|
||||
const errorCountEl = document.getElementById('shift-error-count');
|
||||
const progressBar = document.getElementById('shift-email-progress-bar');
|
||||
const progressText = document.getElementById('shift-progress-text');
|
||||
const closeBtn = document.getElementById('close-shift-progress-btn');
|
||||
|
||||
// Show progress container
|
||||
progressContainer.classList.add('show');
|
||||
|
||||
// Reset counters
|
||||
pendingCountEl.textContent = totalCount;
|
||||
successCountEl.textContent = '0';
|
||||
errorCountEl.textContent = '0';
|
||||
|
||||
// Reset progress bar
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.classList.remove('complete', 'error');
|
||||
progressText.textContent = '0%';
|
||||
|
||||
// Clear status list
|
||||
statusList.innerHTML = '';
|
||||
|
||||
// Hide close button initially
|
||||
closeBtn.style.display = 'none';
|
||||
|
||||
// Add status items for each volunteer
|
||||
const volunteers = currentShiftData.signups || [];
|
||||
volunteers.forEach(volunteer => {
|
||||
const statusItem = document.createElement('div');
|
||||
statusItem.className = 'email-status-item';
|
||||
statusItem.innerHTML = `
|
||||
<div class="email-status-recipient">${volunteer['User Name'] || volunteer['User Email']}</div>
|
||||
<div class="email-status-result pending">
|
||||
<div class="progress-spinner"></div>
|
||||
<span>Sending...</span>
|
||||
</div>
|
||||
`;
|
||||
statusList.appendChild(statusItem);
|
||||
});
|
||||
}
|
||||
|
||||
// Update shift email progress with results
|
||||
function updateShiftEmailProgress(results) {
|
||||
const statusList = document.getElementById('shift-email-status-list');
|
||||
const pendingCountEl = document.getElementById('shift-pending-count');
|
||||
const successCountEl = document.getElementById('shift-success-count');
|
||||
const errorCountEl = document.getElementById('shift-error-count');
|
||||
const progressBar = document.getElementById('shift-email-progress-bar');
|
||||
const progressText = document.getElementById('shift-progress-text');
|
||||
const closeBtn = document.getElementById('close-shift-progress-btn');
|
||||
|
||||
const successful = results.successful || [];
|
||||
const failed = results.failed || [];
|
||||
const total = results.total || (successful.length + failed.length);
|
||||
|
||||
// Update counters
|
||||
successCountEl.textContent = successful.length;
|
||||
errorCountEl.textContent = failed.length;
|
||||
pendingCountEl.textContent = '0';
|
||||
|
||||
// Update progress bar
|
||||
const percentage = ((successful.length + failed.length) / total * 100).toFixed(1);
|
||||
progressBar.style.width = percentage + '%';
|
||||
progressText.textContent = percentage + '%';
|
||||
|
||||
if (failed.length > 0) {
|
||||
progressBar.classList.add('error');
|
||||
} else {
|
||||
progressBar.classList.add('complete');
|
||||
}
|
||||
|
||||
// Update individual status items
|
||||
const statusItems = statusList.children;
|
||||
|
||||
// Update successful emails
|
||||
successful.forEach(result => {
|
||||
const statusItem = Array.from(statusItems).find(item =>
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
|
||||
);
|
||||
if (statusItem) {
|
||||
statusItem.querySelector('.email-status-result').innerHTML = `
|
||||
<span class="email-status-result success">✓ Sent</span>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Update failed emails
|
||||
failed.forEach(result => {
|
||||
const statusItem = Array.from(statusItems).find(item =>
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
|
||||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
|
||||
);
|
||||
if (statusItem) {
|
||||
statusItem.querySelector('.email-status-result').innerHTML = `
|
||||
<span class="email-status-result error" title="${result.error || 'Unknown error'}">✗ Failed</span>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Show close button
|
||||
closeBtn.style.display = 'block';
|
||||
closeBtn.onclick = () => {
|
||||
document.getElementById('shift-email-progress-container').classList.remove('show');
|
||||
};
|
||||
}
|
||||
|
||||
// Show shift email error
|
||||
function showShiftEmailError(message) {
|
||||
const progressContainer = document.getElementById('shift-email-progress-container');
|
||||
const progressBar = document.getElementById('shift-email-progress-bar');
|
||||
const progressText = document.getElementById('shift-progress-text');
|
||||
const closeBtn = document.getElementById('close-shift-progress-btn');
|
||||
|
||||
// Show progress container if not visible
|
||||
progressContainer.classList.add('show');
|
||||
|
||||
// Update progress bar to show error
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.classList.add('error');
|
||||
progressText.textContent = 'Error';
|
||||
|
||||
// Show close button
|
||||
closeBtn.style.display = 'block';
|
||||
closeBtn.onclick = () => {
|
||||
progressContainer.classList.remove('show');
|
||||
};
|
||||
|
||||
showStatus(message, 'error');
|
||||
}
|
||||
|
||||
// Setup modal event listeners when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const closeModalBtn = document.getElementById('close-user-modal');
|
||||
const addUserBtn = document.getElementById('add-user-btn');
|
||||
const emailShiftDetailsBtn = document.getElementById('email-shift-details-btn');
|
||||
const modal = document.getElementById('shift-user-modal');
|
||||
|
||||
if (closeModalBtn) {
|
||||
closeModalBtn.addEventListener('click', closeShiftUserModal);
|
||||
}
|
||||
|
||||
if (addUserBtn) {
|
||||
addUserBtn.addEventListener('click', addUserToShift);
|
||||
}
|
||||
|
||||
if (emailShiftDetailsBtn) {
|
||||
emailShiftDetailsBtn.addEventListener('click', emailShiftDetails);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
if (modal) {
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) {
|
||||
closeShiftUserModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Setup email users modal event listeners when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Email all users functionality
|
||||
const closeEmailModalBtn = document.getElementById('close-email-modal');
|
||||
const cancelEmailBtn = document.getElementById('cancel-email-btn');
|
||||
const emailUsersForm = document.getElementById('email-users-form');
|
||||
const emailModal = document.getElementById('email-users-modal');
|
||||
|
||||
if (closeEmailModalBtn) {
|
||||
closeEmailModalBtn.addEventListener('click', closeEmailUsersModal);
|
||||
}
|
||||
|
||||
if (cancelEmailBtn) {
|
||||
cancelEmailBtn.addEventListener('click', closeEmailUsersModal);
|
||||
}
|
||||
|
||||
if (emailUsersForm) {
|
||||
emailUsersForm.addEventListener('submit', sendEmailToAllUsers);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
if (emailModal) {
|
||||
emailModal.addEventListener('click', function(e) {
|
||||
if (e.target === emailModal) {
|
||||
closeEmailUsersModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup rich text editor functionality
|
||||
setupRichTextEditor();
|
||||
});
|
||||
|
||||
@@ -15,4 +15,11 @@ router.post('/admin', requireAdmin, shiftsController.create);
|
||||
router.put('/admin/:id', requireAdmin, shiftsController.update);
|
||||
router.delete('/admin/:id', requireAdmin, shiftsController.delete);
|
||||
|
||||
// Admin user management for shifts
|
||||
router.post('/admin/:shiftId/add-user', requireAdmin, shiftsController.addUserToShift);
|
||||
router.delete('/admin/:shiftId/remove-user/:userId', requireAdmin, shiftsController.removeUserFromShift);
|
||||
|
||||
// Admin email functionality
|
||||
router.post('/admin/:shiftId/email-details', requireAdmin, shiftsController.emailShiftDetails);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,6 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const usersController = require('../controllers/usersController');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// All user routes require admin access
|
||||
router.use(requireAdmin);
|
||||
|
||||
// Get all users
|
||||
router.get('/', usersController.getAll);
|
||||
@@ -11,6 +15,9 @@ router.post('/', usersController.create);
|
||||
// Send login details to user
|
||||
router.post('/:id/send-login-details', usersController.sendLoginDetails);
|
||||
|
||||
// Email all users
|
||||
router.post('/email-all', usersController.emailAllUsers);
|
||||
|
||||
// Delete user
|
||||
router.delete('/:id', usersController.delete);
|
||||
|
||||
|
||||
156
map/app/templates/email/shift-details.html
Normal file
156
map/app/templates/email/shift-details.html
Normal file
@@ -0,0 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Shift Details - {{SHIFT_TITLE}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.logo {
|
||||
color: #a02c8d;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.shift-details-box {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.detail-row {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.detail-icon {
|
||||
width: 30px;
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.detail-label {
|
||||
font-weight: bold;
|
||||
min-width: 80px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.detail-value {
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
}
|
||||
.description-section {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.highlight {
|
||||
background-color: #fff3cd;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #ffc107;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.status-open {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status-full {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">{{APP_NAME}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Shift Details: {{SHIFT_TITLE}}</h2>
|
||||
<p>Hello {{USER_NAME}},</p>
|
||||
<p>Here are the details for your upcoming volunteer shift:</p>
|
||||
|
||||
<div class="shift-details-box">
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">📅</span>
|
||||
<span class="detail-label">Date:</span>
|
||||
<span class="detail-value">{{SHIFT_DATE}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">⏰</span>
|
||||
<span class="detail-label">Time:</span>
|
||||
<span class="detail-value">{{SHIFT_START_TIME}} - {{SHIFT_END_TIME}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">📍</span>
|
||||
<span class="detail-label">Location:</span>
|
||||
<span class="detail-value">{{SHIFT_LOCATION}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">👥</span>
|
||||
<span class="detail-label">Volunteers:</span>
|
||||
<span class="detail-value">{{CURRENT_VOLUNTEERS}}/{{MAX_VOLUNTEERS}} signed up</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">📊</span>
|
||||
<span class="detail-label">Status:</span>
|
||||
<span class="detail-value">
|
||||
<span class="status-badge status-{{SHIFT_STATUS_CLASS}}">{{SHIFT_STATUS}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="description-section" id="description-section"{{DESCRIPTION_DISPLAY_STYLE}}>
|
||||
<h3>Additional Information:</h3>
|
||||
<p>{{SHIFT_DESCRIPTION}}</p>
|
||||
</div>
|
||||
|
||||
<div class="highlight">
|
||||
<p><strong>Important:</strong> Please arrive 10-15 minutes early and wear weather appropriate wear. If you need to cancel, please do so as early as possible to allow other volunteers to sign up.</p>
|
||||
</div>
|
||||
|
||||
<p>Questions? Contact your shift coordinator or administrator.</p>
|
||||
<p>Thank you for volunteering!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This email was sent from {{APP_NAME}} at {{TIMESTAMP}}</p>
|
||||
<p>Volunteer shift management system</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
30
map/app/templates/email/shift-details.txt
Normal file
30
map/app/templates/email/shift-details.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
Shift Details - {{SHIFT_TITLE}}
|
||||
|
||||
Hello {{USER_NAME}},
|
||||
|
||||
Here are the details for your upcoming volunteer shift:
|
||||
|
||||
SHIFT INFORMATION:
|
||||
==================
|
||||
Title: {{SHIFT_TITLE}}
|
||||
Date: {{SHIFT_DATE}}
|
||||
Time: {{SHIFT_START_TIME}} - {{SHIFT_END_TIME}}
|
||||
Location: {{SHIFT_LOCATION}}
|
||||
Volunteers: {{CURRENT_VOLUNTEERS}}/{{MAX_VOLUNTEERS}} signed up
|
||||
Status: {{SHIFT_STATUS}}
|
||||
|
||||
{{SHIFT_DESCRIPTION_SECTION}}
|
||||
|
||||
IMPORTANT NOTES:
|
||||
===============
|
||||
- Please arrive 10-15 minutes early
|
||||
- Bring photo ID
|
||||
- If you need to cancel, please do so as early as possible to allow other volunteers to sign up
|
||||
|
||||
Questions? Contact your shift coordinator or administrator.
|
||||
|
||||
Thank you for volunteering!
|
||||
|
||||
---
|
||||
This email was sent from {{APP_NAME}} at {{TIMESTAMP}}
|
||||
Volunteer shift management system
|
||||
109
map/app/templates/email/user-broadcast.html
Normal file
109
map/app/templates/email/user-broadcast.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{EMAIL_SUBJECT}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.logo {
|
||||
color: #a02c8d;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.message-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
.message-content h1,
|
||||
.message-content h2,
|
||||
.message-content h3 {
|
||||
color: #2c3e50;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.message-content h1 {
|
||||
font-size: 24px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.message-content h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.message-content h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.message-content ul,
|
||||
.message-content ol {
|
||||
margin: 15px 0;
|
||||
padding-left: 25px;
|
||||
}
|
||||
.message-content li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.message-content a {
|
||||
color: #a02c8d;
|
||||
text-decoration: none;
|
||||
}
|
||||
.message-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.message-content strong {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.message-content blockquote {
|
||||
border-left: 4px solid #a02c8d;
|
||||
margin: 20px 0;
|
||||
padding: 10px 20px;
|
||||
background-color: #f8f9fa;
|
||||
font-style: italic;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">{{APP_NAME}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello {{USER_NAME}},</p>
|
||||
|
||||
<div class="message-content">
|
||||
{{EMAIL_CONTENT}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This email was sent from {{APP_NAME}} at {{TIMESTAMP}}</p>
|
||||
<p>{{SENDER_NAME}} - System Administrator</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
9
map/app/templates/email/user-broadcast.txt
Normal file
9
map/app/templates/email/user-broadcast.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
{{EMAIL_SUBJECT}}
|
||||
|
||||
Hello {{USER_NAME}},
|
||||
|
||||
{{EMAIL_CONTENT_TEXT}}
|
||||
|
||||
---
|
||||
This email was sent from {{APP_NAME}} at {{TIMESTAMP}}
|
||||
{{SENDER_NAME}} - System Administrator
|
||||
Reference in New Issue
Block a user