server/controllers/shift.controller.js

const User = require('../models/user.model');
const Shift = require('../models/shift.model');
const escape = require('escape-html');
const aws = require('aws-sdk');
const { createNotification } = require('./notification.controller');

aws.config.update({
    secretAccessKey: process.env.S3_ACCESS_SECRET,
    accessKeyId: process.env.S3_ACCESS_KEY,
    region: process.env.S3_DEFAULT_REGION,
});
const s3 = new aws.S3();

/** @module shift_controller */

/**
 * This function creates a shift.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Created shift
 */
exports.createShift = async (req, res) => {
    const email = escape(req.body.teacher);
    const startTime = new Date(escape(req.body.startTime));
    const endTime = new Date(escape(req.body.endTime));
    const subject = escape(req.body.subject);

    try {
        // Check if teacher exists
        let teacher = await User.findOne({ email }).lean();
        if (!teacher)
            return res.status(404).json(`Teacher not found: ${email}`);
        if (teacher.site != req.user.site)
            return res.status(400).json('Teacher does not belong to this site');

        // Check if shift already exists
        let existingShift = await Shift.findOne({
            teacher: teacher._id,
            startTime,
            endTime,
        }).lean();
        if (existingShift) return res.status(400).json('Shift already exists');

        const shiftObj = {
            teacher: escape(teacher._id),
            startTime,
            endTime,
            subject,
            site: escape(req.user.site),
        };
        if (req.body.details) shiftObj.details = escape(req.body.details);

        const shift = await Shift.create(shiftObj);
        await shift.populate('teacher', 'firstName lastName email');

        return res.status(201).json(shift);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function returns shift by shift Id.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Shift corresponding to the id
 */
exports.getShiftById = async (req, res) => {
    const shiftId = escape(req.params.shiftId);

    try {
        const shift = await Shift.findById(shiftId).lean();
        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function returns shifts by site.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object[]} - Shifts belonging to the user's site
 */
exports.getShiftsBySite = async (req, res) => {
    try {
        const shifts = await Shift.find({ site: req.user.site })
            .lean()
            .populate('teacher', 'firstName lastName email')
            .populate('sub', 'firstName lastName email');
        return res.status(200).json(shifts);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function returns shifts belonging to the user.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object[]} - Shifts belonging to the user
 */
exports.getShiftsByUser = async (req, res) => {
    try {
        const shifts = await Shift.find({
            site: req.user.site,
            $or: [
                { teacher: req.user._id },
                {
                    $and: [
                        { $not: { teacher: req.user._id } },
                        { sub: req.user._id },
                    ],
                },
            ],
        })
            .lean()
            .populate('teacher', 'firstName lastName email')
            .populate('sub', 'firstName lastName email');

        return res.status(200).json(shifts);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function returns posted shifts by site.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object[]} - Posted shifts belonging to the user's site
 */
exports.getPostedShiftsBySite = async (req, res) => {
    try {
        const shifts = await Shift.find({
            site: req.user.site,
            posted: true,
            teacher: { $ne: req.user._id },
        })
            .lean()
            .populate('teacher', 'firstName lastName email')
            .populate('sub', 'firstName lastName email');
        return res.status(200).json(shifts);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function updates shifts by shift id.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Updated shift
 */
exports.updateShiftById = async (req, res) => {
    const updateQuery = {};
    if (req.body.teacher) updateQuery.teacher = escape(req.body.teacher);
    if (req.body.sub) updateQuery.sub = escape(req.body.sub);
    if (req.body.subject) updateQuery.subject = escape(req.body.subject);
    if (req.body.details) updateQuery.details = escape(req.body.details);
    if (req.body.startTime) updateQuery.startTime = escape(req.body.startTime);
    if (req.body.endTime) updateQuery.endTime = escape(req.body.endTime);
    if (req.body.site) updateQuery.site = escape(req.body.site);

    const shiftId = escape(req.params.shiftId);

    if (updateQuery.subject?.length > 20) {
        return res
            .status(400)
            .json('Subject cannot be more than 20 characters');
    }

    if (new Date(updateQuery.startTime) >= new Date(updateQuery.endTime)) {
        return res.status(400).json('Start time must be before end time');
    }

    try {
        const shift = await Shift.findByIdAndUpdate(shiftId, updateQuery, {
            new: true,
        });
        await shift.populate('teacher', 'firstName lastName email');
        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function retrieves materials from a shift via S3.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object[]} - Materials from the shift
 */
exports.getShiftMaterials = async (req, res) => {
    const shiftId = escape(req.params.shiftId);
    const fileKey = escape(req.params.fileKey);

    try {
        const shift = await Shift.findById(shiftId).lean();
        shift.materials.forEach((material) => {
            if (material.fileKey === fileKey) {
                const downloadParams = {
                    Key: material.fileKey,
                    Bucket: process.env.S3_SHIFT_BUCKET,
                };
                const readStream = s3
                    .getObject(downloadParams)
                    .createReadStream();
                readStream.pipe(res.attachment(material.fileName));
            }
        });
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function updates shift materials, to be used after s3 upload middleware.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Updated shift with the uploaded materials
 */

exports.updateShiftMaterials = async (req, res) => {
    if (!req.files) return res.status(400).send('No files uploaded');
    const shiftId = escape(req.params.shiftId);
    let updateQuery;
    try {
        let shift = await Shift.findById(shiftId).lean();
        updateQuery = { materials: shift.materials };
    } catch (err) {
        return res.status(400).send('Failed to get Current Files');
    }

    req.files.forEach((file) => {
        updateQuery.materials.push({
            fileName: file.originalname,
            fileKey: file.key,
        });
    });

    try {
        const shift = await Shift.findByIdAndUpdate(shiftId, updateQuery, {
            new: true,
        }).populate('teacher', 'firstName lastName email');
        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).send(err.message);
    }
};

/**
 * This function deletes a single material from a shift via S3.
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Updated shift without the deleted material
 */
exports.deleteShiftMaterial = async (req, res) => {
    const shiftId = escape(req.params.shiftId);
    const fileKey = escape(req.params.fileKey);
    const deleteParams = {
        Key: fileKey,
        Bucket: process.env.S3_SHIFT_BUCKET,
    };

    let updateQuery = {};
    try {
        let shift = await Shift.findById(shiftId).lean();
        updateQuery = {
            materials: shift.materials.filter(
                (material) => material.fileKey != fileKey
            ),
        };
        await s3.deleteObject(deleteParams).promise();
    } catch (err) {
        console.error(err);
    }

    try {
        const shift = await Shift.findByIdAndUpdate(shiftId, updateQuery, {
            new: true,
        }).populate('teacher', 'firstName lastName email');
        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).send(err.message);
    }
};

/**
 * This function deletes all shifts from a site
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {String} - Message confirming deletion
 */
exports.deleteShiftsBySite = async (req, res) => {
    try {
        await Shift.deleteMany({ site: req.user.site });
        return res.status(200).json('Shifts successfully deleted');
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function posts a shift
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Shift posted
 */
exports.postShift = async (req, res) => {
    try {
        const shift = await Shift.findByIdAndUpdate(
            escape(req.params.shiftId),
            { posted: true },
            { new: true }
        ).populate('teacher', 'firstName lastName email');

        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function unposts a shift
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Shift unposted
 */
exports.unpostShift = async (req, res) => {
    const shiftId = escape(req.params.shiftId);
    try {
        const shift = await Shift.findById(shiftId);

        if (shift.sub) return res.status(400).json('Shift has been taken.');

        shift.posted = false;
        shift.populate('teacher', 'firstName lastName email');

        await shift.save();

        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function takes a shift
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Shift taken
 */
exports.takeShift = async (req, res) => {
    const shiftId = escape(req.params.shiftId);
    const updateQuery = { posted: false, sub: req.user._id };

    try {
        const shift = await Shift.findByIdAndUpdate(shiftId, updateQuery, {
            new: true,
        })
            .populate('teacher', 'firstName lastName email')
            .populate('sub', 'firstName lastName email');

        if (req.user._id !== shift.teacher._id) {
            createNotification(shift.sub, shift.teacher, `Shift`, shift);
        }

        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};

/**
 * This function returns a shift
 *
 * @function
 * @async
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Object} - Shift returned
 */
exports.returnShift = async (req, res) => {
    const shiftId = escape(req.params.shiftId);

    try {
        let shift = await Shift.findById(shiftId)
            .populate('teacher', 'firstName lastName email')
            .populate('sub', 'firstName lastName email');

        if (req.user._id != shift.sub._id) {
            return res.status(400).json('Must be substitute to return shift.');
        }

        const shiftSub = shift.sub;

        shift.posted = true;
        shift.sub = null;
        await shift.save();

        createNotification(shiftSub, shift.teacher, `Shift_Return`, shift);

        return res.status(200).json(shift);
    } catch (err) {
        return res.status(400).json(err.message);
    }
};