Source: controllers/ActivitiesController.mjs

import express from "express";
import { ActivitiesModel } from "../models/ActivitiesModel.mjs";
import { AuthenticationController } from "./AuthenticationController.mjs";
import { body } from "express-validator";
import { management } from "../utilities/formValidation.mjs";

/** HTTP handlers for activities.
 * @class
 */
export class ActivitiesController {
  /** @type {express.Router} */
  static routes = express.Router();

  /**
   * Validation for the activity management form. Field rules run for create
   * and update only; limits match the activities table columns.
   * @type {express.RequestHandler[]}
   */
  static formValidation = management(
    "/activities",
    ["name", "description"],
    [
      body("name")
        .isString()
        .withMessage("Name must be text.")
        .bail()
        .trim()
        .isLength({ min: 1, max: 45 })
        .withMessage("Name must contain 1-45 characters."),
      body("description")
        .isString()
        .withMessage("Must have a description.")
        .bail()
        .trim()
        .isLength({ min: 1, max: 150 })
        .withMessage("Description must contain 1-150 characters."),
      body("deleted")
        .optional()
        .custom(
          (value) => typeof value === "string" || Number.isSafeInteger(value),
        )
        .withMessage("Deleted must be a valid number.")
        .bail()
        .isInt({ min: 0, max: 1, allow_leading_zeroes: false })
        .withMessage("Deleted must be an integer between 0 and 1."),
      body("updatedBy")
        .optional()
        .custom(
          (value) => typeof value === "string" || Number.isSafeInteger(value),
        )
        .withMessage("Updated by must be a valid number.")
        .bail()
        .isInt({ min: 0, max: 2147483647, allow_leading_zeroes: false })
        .withMessage("Updated by must be an integer between 0 and 2147483647."),
    ],
    { updatedBy: "updated_by" },
  );

  static {
    this.routes.get(
      "/",
      AuthenticationController.restrict("admin"),
      this.viewActivityManagement,
    );
    this.routes.get(
      "/:id",
      AuthenticationController.restrict("admin"),
      this.viewActivityManagement,
    );
    this.routes.post(
      "/",
      AuthenticationController.restrict("admin"),
      this.formValidation,
      this.handleActivityManagement,
    );
    this.routes.post(
      "/:id",
      AuthenticationController.restrict("admin"),
      this.formValidation,
      this.handleActivityManagement,
    );
  }

  /**
   * Renders the activity management page with a filtered, sorted and paginated
   * activity list. Loads the activity in the URL into the edit form, even when it
   * is not on the current page.
   * @type {express.RequestHandler}
   */
  static async viewActivityManagement(req, res) {
    const selectedSearchTerm = String(req.query.search_term ?? "").trim();
    const selectedSortBy = Object.keys(
      ActivitiesModel.SORTABLE_COLUMNS,
    ).includes(req.query.sort_by)
      ? req.query.sort_by
      : "name";
    const selectedSortDir = req.query.sort_dir === "desc" ? "desc" : "asc";
    const pageSize = 7;
    const selectedPage = Math.max(1, Number(req.query.page) || 1);
    try {
      const { activities, total } = await ActivitiesModel.list({
        searchTerm: selectedSearchTerm,
        sortBy: selectedSortBy,
        sortDir: selectedSortDir,
        page: selectedPage,
        pageSize,
      });
      const selectedActivity =
        activities.find((activity) => String(activity.id) === req.params.id) ??
        (req.params.id
          ? await ActivitiesModel.getById(req.params.id).catch(() => null)
          : null) ??
        new ActivitiesModel(null, "", "", 0, 0);
      res.render("activity_management.ejs", {
        activities,
        selectedActivity,
        selectedSearchTerm,
        selectedSortBy,
        selectedSortDir,
        selectedPage,
        totalPages: Math.max(1, Math.ceil(total / pageSize)),
        authenticatedUser: req.authenticatedUser ?? {},
        role: "admin",
      });
    } catch {
      res.status(500).render("status.ejs", {
        status: "Database Error",
        message: "Activities could not be loaded.",
      });
    }
  }

  /**
   * Creates, updates or deletes an activity from the validated management form.
   * @type {express.RequestHandler}
   */
  static async handleActivityManagement(req, res) {
    const activity = new ActivitiesModel(
      req.params.id ? Number(req.params.id) : null,
      req.body.name,
      req.body.description,
      Number(req.body.deleted ?? 0),
      Number(req.body.updatedBy ?? 0),
    );

    try {
      if (req.body.action === "create") {
        await ActivitiesModel.create(activity);
        return res.redirect("/activities");
      }
      if (req.body.action === "update") {
        const result = await ActivitiesModel.update(activity);
        return result.affectedRows > 0
          ? res.redirect("/activities")
          : res.status(404).render("status.ejs", {
              status: "Activity Update Failed",
              message: "The activity could not be found.",
            });
      }
      if (req.body.action === "delete") {
        const result = await ActivitiesModel.delete(activity.id);
        return result.affectedRows > 0
          ? res.redirect("/activities")
          : res.status(404).render("status.ejs", {
              status: "Activity Deletion Failed",
              message: "The activity could not be found.",
            });
      }
      return res.status(400).render("status.ejs", {
        status: "Invalid Action",
        message: "The form doesn't support this action.",
      });
    } catch {
      return res.status(500).render("status.ejs", {
        status: "Database Error",
        message: "The activity could not be saved.",
      });
    }
  }

  /** @type {express.RequestHandler} */
  static async list(req, res, next) {
    try {
      const activities = req.query.search_term
        ? await ActivitiesModel.getBySearch(req.query.search_term)
        : await ActivitiesModel.getAll();
      res.json(activities);
    } catch (error) {
      next(error);
    }
  }

  /** @type {express.RequestHandler} */
  static async getById(req, res, next) {
    try {
      res.json(await ActivitiesModel.getById(Number(req.params.id)));
    } catch (error) {
      next(error);
    }
  }

  /** @type {express.RequestHandler} */
  static async create(req, res, next) {
    try {
      res.status(201).json(await ActivitiesModel.create(req.body));
    } catch (error) {
      next(error);
    }
  }

  /** @type {express.RequestHandler} */
  static async update(req, res, next) {
    try {
      res.json(
        await ActivitiesModel.update({
          ...req.body,
          id: Number(req.params.id),
        }),
      );
    } catch (error) {
      next(error);
    }
  }

  /** @type {express.RequestHandler} */
  static async delete(req, res, next) {
    try {
      res.json(await ActivitiesModel.delete(Number(req.params.id)));
    } catch (error) {
      next(error);
    }
  }
}