714 lines
30 KiB
JavaScript
714 lines
30 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.shiftsService = void 0;
|
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
const client_1 = require("@prisma/client");
|
|
const database_1 = require("../../../config/database");
|
|
const error_handler_1 = require("../../../middleware/error-handler");
|
|
const email_service_1 = require("../../../services/email.service");
|
|
const settings_service_1 = require("../../settings/settings.service");
|
|
const env_1 = require("../../../config/env");
|
|
const logger_1 = require("../../../utils/logger");
|
|
const metrics_1 = require("../../../utils/metrics");
|
|
const adjectives = ['Blue', 'Red', 'Green', 'Swift', 'Bright', 'Bold', 'Calm', 'Fair'];
|
|
const nouns = ['Eagle', 'River', 'Mountain', 'Star', 'Forest', 'Lake', 'Wolf', 'Hawk'];
|
|
function generateReadablePassword() {
|
|
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
const num = Math.floor(Math.random() * 90) + 10;
|
|
return `${adj}${noun}${num}`;
|
|
}
|
|
exports.shiftsService = {
|
|
async findAll(filters) {
|
|
const { page, limit, search, status, upcoming, sortBy, sortOrder } = filters;
|
|
const skip = (page - 1) * limit;
|
|
const where = {};
|
|
if (search) {
|
|
where.OR = [
|
|
{ title: { contains: search, mode: 'insensitive' } },
|
|
{ location: { contains: search, mode: 'insensitive' } },
|
|
];
|
|
}
|
|
if (status)
|
|
where.status = status;
|
|
if (upcoming) {
|
|
where.date = { gte: new Date() };
|
|
}
|
|
const orderBy = sortBy === 'title'
|
|
? { title: sortOrder }
|
|
: sortBy === 'createdAt'
|
|
? { createdAt: sortOrder }
|
|
: { date: sortOrder };
|
|
const [shifts, total] = await Promise.all([
|
|
database_1.prisma.shift.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy,
|
|
include: {
|
|
cut: { select: { id: true, name: true } },
|
|
_count: {
|
|
select: {
|
|
signups: { where: { status: client_1.SignupStatus.CONFIRMED } },
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
database_1.prisma.shift.count({ where }),
|
|
]);
|
|
return {
|
|
shifts,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
},
|
|
async findById(id) {
|
|
const shift = await database_1.prisma.shift.findUnique({
|
|
where: { id },
|
|
include: {
|
|
cut: { select: { id: true, name: true } },
|
|
signups: {
|
|
where: { status: client_1.SignupStatus.CONFIRMED },
|
|
include: { user: { select: { id: true, email: true, name: true, phone: true } } },
|
|
orderBy: { signupDate: 'desc' },
|
|
},
|
|
_count: {
|
|
select: {
|
|
signups: { where: { status: client_1.SignupStatus.CONFIRMED } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!shift) {
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
}
|
|
return shift;
|
|
},
|
|
async create(data, userId) {
|
|
const shift = await database_1.prisma.shift.create({
|
|
data: {
|
|
title: data.title,
|
|
description: data.description,
|
|
date: new Date(data.date),
|
|
startTime: data.startTime,
|
|
endTime: data.endTime,
|
|
location: data.location,
|
|
maxVolunteers: data.maxVolunteers,
|
|
isPublic: data.isPublic,
|
|
cutId: data.cutId,
|
|
createdBy: userId,
|
|
},
|
|
});
|
|
return shift;
|
|
},
|
|
async update(id, data) {
|
|
const existing = await database_1.prisma.shift.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
}
|
|
const updateData = { ...data };
|
|
if (data.date) {
|
|
updateData.date = new Date(data.date);
|
|
}
|
|
// Auto-update status when capacity changes
|
|
if (data.maxVolunteers !== undefined) {
|
|
if (existing.currentVolunteers >= data.maxVolunteers && existing.status === client_1.ShiftStatus.OPEN) {
|
|
updateData.status = client_1.ShiftStatus.FULL;
|
|
}
|
|
else if (existing.currentVolunteers < data.maxVolunteers && existing.status === client_1.ShiftStatus.FULL) {
|
|
updateData.status = client_1.ShiftStatus.OPEN;
|
|
}
|
|
}
|
|
const shift = await database_1.prisma.shift.update({
|
|
where: { id },
|
|
data: updateData,
|
|
});
|
|
return shift;
|
|
},
|
|
async delete(id) {
|
|
const existing = await database_1.prisma.shift.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
}
|
|
await database_1.prisma.shift.delete({ where: { id } });
|
|
},
|
|
async getStats() {
|
|
const [total, open, full, cancelled, upcoming, totalSignups] = await Promise.all([
|
|
database_1.prisma.shift.count(),
|
|
database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.OPEN } }),
|
|
database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.FULL } }),
|
|
database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.CANCELLED } }),
|
|
database_1.prisma.shift.count({ where: { date: { gte: new Date() }, status: { not: client_1.ShiftStatus.CANCELLED } } }),
|
|
database_1.prisma.shiftSignup.count({ where: { status: client_1.SignupStatus.CONFIRMED } }),
|
|
]);
|
|
return { total, open, full, cancelled, upcoming, totalSignups };
|
|
},
|
|
async getSignups(shiftId) {
|
|
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
|
if (!shift) {
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
}
|
|
return database_1.prisma.shiftSignup.findMany({
|
|
where: { shiftId },
|
|
include: { user: { select: { id: true, email: true, name: true, phone: true } } },
|
|
orderBy: { signupDate: 'desc' },
|
|
});
|
|
},
|
|
async addSignup(shiftId, data) {
|
|
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
|
if (!shift) {
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
}
|
|
if (shift.currentVolunteers >= shift.maxVolunteers) {
|
|
throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
|
|
}
|
|
// Check unique constraint
|
|
const existing = await database_1.prisma.shiftSignup.findUnique({
|
|
where: { shiftId_userEmail: { shiftId, userEmail: data.userEmail } },
|
|
});
|
|
if (existing) {
|
|
if (existing.status === client_1.SignupStatus.CANCELLED) {
|
|
// Re-activate cancelled signup
|
|
const [signup] = await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.update({
|
|
where: { id: existing.id },
|
|
data: { status: client_1.SignupStatus.CONFIRMED, signupSource: client_1.SignupSource.ADMIN },
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { increment: 1 },
|
|
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
|
},
|
|
}),
|
|
]);
|
|
return signup;
|
|
}
|
|
throw new error_handler_1.AppError(409, 'Volunteer already signed up', 'DUPLICATE_SIGNUP');
|
|
}
|
|
// Look up user
|
|
const user = await database_1.prisma.user.findUnique({ where: { email: data.userEmail } });
|
|
const [signup] = await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.create({
|
|
data: {
|
|
shiftId,
|
|
shiftTitle: shift.title,
|
|
userId: user?.id,
|
|
userEmail: data.userEmail,
|
|
userName: data.userName || user?.name,
|
|
signupSource: client_1.SignupSource.ADMIN,
|
|
},
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { increment: 1 },
|
|
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
|
},
|
|
}),
|
|
]);
|
|
return signup;
|
|
},
|
|
async removeSignup(signupId) {
|
|
const signup = await database_1.prisma.shiftSignup.findUnique({ where: { id: signupId } });
|
|
if (!signup) {
|
|
throw new error_handler_1.AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
|
|
}
|
|
if (signup.status === client_1.SignupStatus.CANCELLED) {
|
|
throw new error_handler_1.AppError(400, 'Signup already cancelled', 'ALREADY_CANCELLED');
|
|
}
|
|
await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.update({
|
|
where: { id: signupId },
|
|
data: { status: client_1.SignupStatus.CANCELLED },
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: signup.shiftId },
|
|
data: {
|
|
currentVolunteers: { decrement: 1 },
|
|
status: client_1.ShiftStatus.OPEN,
|
|
},
|
|
}),
|
|
]);
|
|
},
|
|
async publicSignup(shiftId, data) {
|
|
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
|
if (!shift) {
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
}
|
|
if (!shift.isPublic) {
|
|
throw new error_handler_1.AppError(403, 'This shift is not open for public signup', 'NOT_PUBLIC');
|
|
}
|
|
if (shift.status !== client_1.ShiftStatus.OPEN) {
|
|
throw new error_handler_1.AppError(400, 'This shift is not accepting signups', 'NOT_OPEN');
|
|
}
|
|
if (shift.date < new Date(new Date().toISOString().split('T')[0])) {
|
|
throw new error_handler_1.AppError(400, 'This shift has already passed', 'SHIFT_PAST');
|
|
}
|
|
if (shift.currentVolunteers >= shift.maxVolunteers) {
|
|
throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
|
|
}
|
|
// Check unique constraint
|
|
const existingSignup = await database_1.prisma.shiftSignup.findUnique({
|
|
where: { shiftId_userEmail: { shiftId, userEmail: data.email } },
|
|
});
|
|
if (existingSignup && existingSignup.status === client_1.SignupStatus.CONFIRMED) {
|
|
throw new error_handler_1.AppError(409, 'You are already signed up for this shift', 'DUPLICATE_SIGNUP');
|
|
}
|
|
// Look up existing user
|
|
let user = await database_1.prisma.user.findUnique({ where: { email: data.email } });
|
|
let isNewUser = false;
|
|
let tempPassword;
|
|
if (!user) {
|
|
// Create temp user
|
|
tempPassword = generateReadablePassword();
|
|
const hashedPassword = await bcryptjs_1.default.hash(tempPassword, 12);
|
|
const shiftDate = new Date(shift.date);
|
|
shiftDate.setDate(shiftDate.getDate() + 1);
|
|
user = await database_1.prisma.user.create({
|
|
data: {
|
|
email: data.email,
|
|
password: hashedPassword,
|
|
name: data.name,
|
|
phone: data.phone,
|
|
role: 'TEMP',
|
|
createdVia: 'PUBLIC_SHIFT_SIGNUP',
|
|
expiresAt: shiftDate,
|
|
},
|
|
});
|
|
isNewUser = true;
|
|
}
|
|
// Create signup (or re-activate cancelled one)
|
|
let signup;
|
|
if (existingSignup && existingSignup.status === client_1.SignupStatus.CANCELLED) {
|
|
[signup] = await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.update({
|
|
where: { id: existingSignup.id },
|
|
data: {
|
|
status: client_1.SignupStatus.CONFIRMED,
|
|
signupSource: user ? client_1.SignupSource.AUTHENTICATED : client_1.SignupSource.PUBLIC,
|
|
userName: data.name,
|
|
userPhone: data.phone,
|
|
userId: user.id,
|
|
},
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { increment: 1 },
|
|
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
|
},
|
|
}),
|
|
]);
|
|
}
|
|
else {
|
|
[signup] = await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.create({
|
|
data: {
|
|
shiftId,
|
|
shiftTitle: shift.title,
|
|
userId: user.id,
|
|
userEmail: data.email,
|
|
userName: data.name,
|
|
userPhone: data.phone,
|
|
signupSource: isNewUser ? client_1.SignupSource.PUBLIC : client_1.SignupSource.AUTHENTICATED,
|
|
},
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { increment: 1 },
|
|
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
|
},
|
|
}),
|
|
]);
|
|
}
|
|
// Send confirmation email
|
|
try {
|
|
const shiftDate = new Date(shift.date);
|
|
const dateStr = shiftDate.toLocaleDateString('en-CA', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
const htmlTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'html');
|
|
const txtTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'txt');
|
|
let orgName = 'Changemaker Lite';
|
|
try {
|
|
orgName = (await settings_service_1.siteSettingsService.get()).organizationName || orgName;
|
|
}
|
|
catch { /* use default */ }
|
|
const vars = {
|
|
USER_NAME: data.name,
|
|
USER_EMAIL: data.email,
|
|
SHIFT_TITLE: shift.title,
|
|
SHIFT_DATE: dateStr,
|
|
SHIFT_TIME: `${shift.startTime} — ${shift.endTime}`,
|
|
SHIFT_LOCATION: shift.location || 'TBD',
|
|
IS_NEW_USER: isNewUser ? 'true' : '',
|
|
TEMP_PASSWORD: tempPassword || '',
|
|
LOGIN_URL: `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
|
|
ORGANIZATION_NAME: orgName,
|
|
};
|
|
const html = email_service_1.emailService.processTemplate(htmlTemplate, vars);
|
|
const text = email_service_1.emailService.processTemplate(txtTemplate, vars);
|
|
await email_service_1.emailService.sendEmail({
|
|
to: data.email,
|
|
subject: `Signup Confirmed — ${shift.title}`,
|
|
html,
|
|
text,
|
|
});
|
|
}
|
|
catch (err) {
|
|
logger_1.logger.error('Failed to send shift signup confirmation email:', err);
|
|
}
|
|
(0, metrics_1.recordShiftSignup)();
|
|
return { signup, isNewUser };
|
|
},
|
|
async cancelPublicSignup(shiftId, userEmail) {
|
|
const signup = await database_1.prisma.shiftSignup.findUnique({
|
|
where: { shiftId_userEmail: { shiftId, userEmail } },
|
|
});
|
|
if (!signup) {
|
|
throw new error_handler_1.AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
|
|
}
|
|
if (signup.status === client_1.SignupStatus.CANCELLED) {
|
|
throw new error_handler_1.AppError(400, 'Signup already cancelled', 'ALREADY_CANCELLED');
|
|
}
|
|
await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.update({
|
|
where: { id: signup.id },
|
|
data: { status: client_1.SignupStatus.CANCELLED },
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { decrement: 1 },
|
|
status: client_1.ShiftStatus.OPEN,
|
|
},
|
|
}),
|
|
]);
|
|
},
|
|
async getUpcomingForVolunteer(userId) {
|
|
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
|
if (!user)
|
|
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
|
const shifts = await database_1.prisma.shift.findMany({
|
|
where: {
|
|
isPublic: true,
|
|
status: { not: client_1.ShiftStatus.CANCELLED },
|
|
date: { gte: new Date(new Date().toISOString().split('T')[0]) },
|
|
},
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
description: true,
|
|
date: true,
|
|
startTime: true,
|
|
endTime: true,
|
|
location: true,
|
|
maxVolunteers: true,
|
|
currentVolunteers: true,
|
|
status: true,
|
|
},
|
|
orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
|
|
});
|
|
// Check signup status for each shift
|
|
const signups = await database_1.prisma.shiftSignup.findMany({
|
|
where: {
|
|
userEmail: user.email,
|
|
shiftId: { in: shifts.map((s) => s.id) },
|
|
status: client_1.SignupStatus.CONFIRMED,
|
|
},
|
|
select: { shiftId: true },
|
|
});
|
|
const signedUpSet = new Set(signups.map((s) => s.shiftId));
|
|
return shifts.map((s) => ({
|
|
...s,
|
|
isSignedUp: signedUpSet.has(s.id),
|
|
}));
|
|
},
|
|
async volunteerSignup(shiftId, userId) {
|
|
const user = await database_1.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { id: true, email: true, name: true, phone: true },
|
|
});
|
|
if (!user)
|
|
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
|
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
|
if (!shift)
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
if (!shift.isPublic)
|
|
throw new error_handler_1.AppError(403, 'This shift is not open for signup', 'NOT_PUBLIC');
|
|
if (shift.status === client_1.ShiftStatus.CANCELLED)
|
|
throw new error_handler_1.AppError(400, 'This shift is cancelled', 'SHIFT_CANCELLED');
|
|
if (shift.date < new Date(new Date().toISOString().split('T')[0]))
|
|
throw new error_handler_1.AppError(400, 'This shift has already passed', 'SHIFT_PAST');
|
|
if (shift.currentVolunteers >= shift.maxVolunteers)
|
|
throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
|
|
const existing = await database_1.prisma.shiftSignup.findUnique({
|
|
where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
|
|
});
|
|
if (existing && existing.status === client_1.SignupStatus.CONFIRMED) {
|
|
throw new error_handler_1.AppError(409, 'Already signed up for this shift', 'DUPLICATE_SIGNUP');
|
|
}
|
|
let signup;
|
|
if (existing && existing.status === client_1.SignupStatus.CANCELLED) {
|
|
[signup] = await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.update({
|
|
where: { id: existing.id },
|
|
data: { status: client_1.SignupStatus.CONFIRMED, signupSource: client_1.SignupSource.AUTHENTICATED },
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { increment: 1 },
|
|
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
|
},
|
|
}),
|
|
]);
|
|
}
|
|
else {
|
|
[signup] = await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.create({
|
|
data: {
|
|
shiftId,
|
|
shiftTitle: shift.title,
|
|
userId: user.id,
|
|
userEmail: user.email,
|
|
userName: user.name,
|
|
userPhone: user.phone,
|
|
signupSource: client_1.SignupSource.AUTHENTICATED,
|
|
},
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { increment: 1 },
|
|
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
|
},
|
|
}),
|
|
]);
|
|
}
|
|
// Send confirmation email
|
|
try {
|
|
const shiftDate = new Date(shift.date);
|
|
const dateStr = shiftDate.toLocaleDateString('en-CA', {
|
|
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
|
});
|
|
const htmlTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'html');
|
|
const txtTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'txt');
|
|
let orgName = 'Changemaker Lite';
|
|
try {
|
|
orgName = (await settings_service_1.siteSettingsService.get()).organizationName || orgName;
|
|
}
|
|
catch { /* default */ }
|
|
const vars = {
|
|
USER_NAME: user.name || user.email,
|
|
USER_EMAIL: user.email,
|
|
SHIFT_TITLE: shift.title,
|
|
SHIFT_DATE: dateStr,
|
|
SHIFT_TIME: `${shift.startTime} — ${shift.endTime}`,
|
|
SHIFT_LOCATION: shift.location || 'TBD',
|
|
IS_NEW_USER: '',
|
|
TEMP_PASSWORD: '',
|
|
LOGIN_URL: `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
|
|
ORGANIZATION_NAME: orgName,
|
|
};
|
|
const html = email_service_1.emailService.processTemplate(htmlTemplate, vars);
|
|
const text = email_service_1.emailService.processTemplate(txtTemplate, vars);
|
|
await email_service_1.emailService.sendEmail({
|
|
to: user.email,
|
|
subject: `Signup Confirmed — ${shift.title}`,
|
|
html,
|
|
text,
|
|
});
|
|
}
|
|
catch (err) {
|
|
logger_1.logger.error('Failed to send volunteer shift signup confirmation email:', err);
|
|
}
|
|
return signup;
|
|
},
|
|
async cancelVolunteerSignup(shiftId, userId) {
|
|
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
|
if (!user)
|
|
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
|
const signup = await database_1.prisma.shiftSignup.findUnique({
|
|
where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
|
|
});
|
|
if (!signup)
|
|
throw new error_handler_1.AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
|
|
if (signup.status === client_1.SignupStatus.CANCELLED)
|
|
throw new error_handler_1.AppError(400, 'Already cancelled', 'ALREADY_CANCELLED');
|
|
await database_1.prisma.$transaction([
|
|
database_1.prisma.shiftSignup.update({
|
|
where: { id: signup.id },
|
|
data: { status: client_1.SignupStatus.CANCELLED },
|
|
}),
|
|
database_1.prisma.shift.update({
|
|
where: { id: shiftId },
|
|
data: {
|
|
currentVolunteers: { decrement: 1 },
|
|
status: client_1.ShiftStatus.OPEN,
|
|
},
|
|
}),
|
|
]);
|
|
},
|
|
async getMySignups(userId) {
|
|
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
|
if (!user)
|
|
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
|
const signups = await database_1.prisma.shiftSignup.findMany({
|
|
where: {
|
|
userEmail: user.email,
|
|
status: client_1.SignupStatus.CONFIRMED,
|
|
shift: {
|
|
date: { gte: new Date(new Date().toISOString().split('T')[0]) },
|
|
status: { not: client_1.ShiftStatus.CANCELLED },
|
|
},
|
|
},
|
|
include: {
|
|
shift: {
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
description: true,
|
|
date: true,
|
|
startTime: true,
|
|
endTime: true,
|
|
location: true,
|
|
maxVolunteers: true,
|
|
currentVolunteers: true,
|
|
status: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: { shift: { date: 'asc' } },
|
|
});
|
|
return signups;
|
|
},
|
|
async getPublicShifts() {
|
|
const shifts = await database_1.prisma.shift.findMany({
|
|
where: {
|
|
isPublic: true,
|
|
status: { not: client_1.ShiftStatus.CANCELLED },
|
|
date: { gte: new Date(new Date().toISOString().split('T')[0]) },
|
|
},
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
description: true,
|
|
date: true,
|
|
startTime: true,
|
|
endTime: true,
|
|
location: true,
|
|
maxVolunteers: true,
|
|
currentVolunteers: true,
|
|
status: true,
|
|
},
|
|
orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
|
|
});
|
|
return shifts;
|
|
},
|
|
async emailShiftDetails(shiftId) {
|
|
const shift = await database_1.prisma.shift.findUnique({
|
|
where: { id: shiftId },
|
|
include: {
|
|
signups: {
|
|
where: { status: client_1.SignupStatus.CONFIRMED },
|
|
},
|
|
},
|
|
});
|
|
if (!shift) {
|
|
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
|
}
|
|
const shiftDate = new Date(shift.date);
|
|
const dateStr = shiftDate.toLocaleDateString('en-CA', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
const htmlTemplate = email_service_1.emailService.loadTemplate('shift-details', 'html');
|
|
const txtTemplate = email_service_1.emailService.loadTemplate('shift-details', 'txt');
|
|
let orgName = 'Changemaker Lite';
|
|
try {
|
|
orgName = (await settings_service_1.siteSettingsService.get()).organizationName || orgName;
|
|
}
|
|
catch { /* use default */ }
|
|
let sent = 0;
|
|
let failed = 0;
|
|
for (const signup of shift.signups) {
|
|
try {
|
|
const vars = {
|
|
USER_NAME: signup.userName || signup.userEmail,
|
|
SHIFT_TITLE: shift.title,
|
|
SHIFT_DATE: dateStr,
|
|
SHIFT_START_TIME: shift.startTime,
|
|
SHIFT_END_TIME: shift.endTime,
|
|
SHIFT_LOCATION: shift.location || 'TBD',
|
|
SHIFT_DESCRIPTION: shift.description || '',
|
|
CURRENT_VOLUNTEERS: shift.currentVolunteers.toString(),
|
|
MAX_VOLUNTEERS: shift.maxVolunteers.toString(),
|
|
SHIFT_STATUS: shift.status,
|
|
ORGANIZATION_NAME: orgName,
|
|
};
|
|
const html = email_service_1.emailService.processTemplate(htmlTemplate, vars);
|
|
const text = email_service_1.emailService.processTemplate(txtTemplate, vars);
|
|
const result = await email_service_1.emailService.sendEmail({
|
|
to: signup.userEmail,
|
|
subject: `Shift Details — ${shift.title}`,
|
|
html,
|
|
text,
|
|
});
|
|
if (result.success) {
|
|
sent++;
|
|
}
|
|
else {
|
|
failed++;
|
|
}
|
|
}
|
|
catch (err) {
|
|
logger_1.logger.error(`Failed to send shift details to ${signup.userEmail}:`, err);
|
|
failed++;
|
|
}
|
|
}
|
|
return { sent, failed };
|
|
},
|
|
async getCalendarData(startDate, endDate) {
|
|
const shifts = await database_1.prisma.shift.findMany({
|
|
where: {
|
|
date: {
|
|
gte: new Date(startDate),
|
|
lte: new Date(endDate),
|
|
},
|
|
},
|
|
include: {
|
|
cut: { select: { id: true, name: true } },
|
|
signups: true,
|
|
series: { select: { id: true, frequency: true } },
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
// Group by date
|
|
const dateMap = {};
|
|
for (const shift of shifts) {
|
|
const dateKey = shift.date.toISOString().split('T')[0];
|
|
if (!dateMap[dateKey]) {
|
|
dateMap[dateKey] = { count: 0, shifts: [] };
|
|
}
|
|
dateMap[dateKey].count++;
|
|
dateMap[dateKey].shifts.push(shift);
|
|
}
|
|
return { dates: dateMap };
|
|
},
|
|
};
|
|
//# sourceMappingURL=shifts.service.js.map
|