From 7a1f168d92b8c4fcb73f1c320dabd31ea08201f4 Mon Sep 17 00:00:00 2001
From: XDerpingxGruntX <41699998+XDerpingxGruntX@users.noreply.github.com>
Date: Sun, 20 Oct 2024 15:56:05 -0600
Subject: [PATCH] v1.2.3 Release
---
controllers/ControllerController.js | 1808 ++++++++++++++-------------
helpers/controllerActivityHelper.js | 428 ++++---
2 files changed, 1201 insertions(+), 1035 deletions(-)
diff --git a/controllers/ControllerController.js b/controllers/ControllerController.js
index 5a2cbe0..049f575 100644
--- a/controllers/ControllerController.js
+++ b/controllers/ControllerController.js
@@ -1,843 +1,953 @@
-import e from 'express';
+import e from "express";
const router = e.Router();
-import User from '../models/User.js';
-import ControllerHours from '../models/ControllerHours.js';
-import Role from '../models/Role.js';
-import VisitApplication from '../models/VisitApplication.js';
-import Absence from '../models/Absence.js';
-import Notification from '../models/Notification.js';
-import transporter from '../config/mailer.js';
-import getUser from '../middleware/getUser.js';
-import auth from '../middleware/auth.js';
-import microAuth from '../middleware/microAuth.js';
-import axios from 'axios';
-import dotenv from 'dotenv';
-import { DateTime as L } from 'luxon'
+import User from "../models/User.js";
+import ControllerHours from "../models/ControllerHours.js";
+import Role from "../models/Role.js";
+import VisitApplication from "../models/VisitApplication.js";
+import Absence from "../models/Absence.js";
+import Notification from "../models/Notification.js";
+import transporter from "../config/mailer.js";
+import getUser from "../middleware/getUser.js";
+import auth from "../middleware/auth.js";
+import microAuth from "../middleware/microAuth.js";
+import axios from "axios";
+import dotenv from "dotenv";
+import { DateTime as L } from "luxon";
dotenv.config();
-router.get('/', async ({res}) => {
- try {
- const home = await User.find({vis: false, cid: { "$nin": [995625] }}).select('-email -idsToken -discordInfo').sort({
- rating: 'desc',
- lname: 'asc',
- fname: 'asc'
- }).populate({
- path: 'certifications',
- options: {
- sort: {order: 'desc'}
- }
- }).populate({
- path: 'roles',
- options: {
- sort: {order: 'asc'}
- }
- }).populate({
- path: 'absence',
- match: {
- expirationDate: {
- $gte: new Date()
- },
- deleted: false
- },
- select: '-reason'
- }).lean({virtuals: true});
-
- const visiting = await User.find({vis: true}).select('-email -idsToken -discordInfo').sort({
- rating: 'desc',
- lname: 'asc',
- fname: 'asc'
- }).populate({
- path: 'certifications',
- options: {
- sort: {order: 'desc'}
- }
- }).populate({
- path: 'roles',
- options: {
- sort: {order: 'asc'}
- }
- }).populate({
- path: 'absence',
- match: {
- expirationDate: {
- $gte: new Date()
- },
- deleted: false
- },
- select: '-reason'
- }).lean({virtuals: true});
-
- if(!home || !visiting) {
- throw {
- code: 503,
- message: "Unable to retrieve controllers"
- };
- }
-
- res.stdRes.data = {home, visiting};
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.get("/", async ({ res }) => {
+ try {
+ const home = await User.find({ vis: false, cid: { $nin: [995625] } })
+ .select("-email -idsToken -discordInfo")
+ .sort({
+ rating: "desc",
+ lname: "asc",
+ fname: "asc",
+ })
+ .populate({
+ path: "certifications",
+ options: {
+ sort: { order: "desc" },
+ },
+ })
+ .populate({
+ path: "roles",
+ options: {
+ sort: { order: "asc" },
+ },
+ })
+ .populate({
+ path: "absence",
+ match: {
+ expirationDate: {
+ $gte: new Date(),
+ },
+ deleted: false,
+ },
+ select: "-reason",
+ })
+ .lean({ virtuals: true });
+
+ const visiting = await User.find({ vis: true })
+ .select("-email -idsToken -discordInfo")
+ .sort({
+ rating: "desc",
+ lname: "asc",
+ fname: "asc",
+ })
+ .populate({
+ path: "certifications",
+ options: {
+ sort: { order: "desc" },
+ },
+ })
+ .populate({
+ path: "roles",
+ options: {
+ sort: { order: "asc" },
+ },
+ })
+ .populate({
+ path: "absence",
+ match: {
+ expirationDate: {
+ $gte: new Date(),
+ },
+ deleted: false,
+ },
+ select: "-reason",
+ })
+ .lean({ virtuals: true });
+
+ if (!home || !visiting) {
+ throw {
+ code: 503,
+ message: "Unable to retrieve controllers",
+ };
+ }
+
+ res.stdRes.data = { home, visiting };
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.get('/staff', async (req, res) => {
- try {
- const users = await User.find().select('fname lname cid roleCodes').sort({
- lname: 'asc',
- fname: 'asc'
- })/*.populate({
+router.get("/staff", async (req, res) => {
+ try {
+ const users = await User.find()
+ .select("fname lname cid roleCodes")
+ .sort({
+ lname: "asc",
+ fname: "asc",
+ }) /*.populate({
path: 'roles',
options: {
sort: {order: 'asc'}
}
- })*/.lean();
-
- if(!users) {
- throw {
- code: 503,
- message: "Unable to retrieve staff members"
- };
- }
-
- const staff = {
- atm: {
- title: "Air Traffic Manager",
- code: "atm",
- users: []
- },
- datm: {
- title: "Deputy Air Traffic Manager",
- code: "datm",
- users: []
- },
- ta: {
- title: "Training Administrator",
- code: "ta",
- users: []
- },
- ec: {
- title: "Events Team",
- code: "ec",
- users: []
- },
- wm: {
- title: "Web Team",
- code: "wm",
- users: []
- },
- fe: {
- title: "Facility Engineer",
- code: "fe",
- users: []
- },
- ins: {
- title: "Instructors",
- code: "instructors",
- users: []
- },
- mtr: {
- title: "Mentors",
- code: "instructors",
- users: []
- },
- dta: {
- title: "Deputy Training Administrator",
- code: "dta",
- users: []
- },
- };
-
- users.forEach(user => user.roleCodes.forEach(role => staff[role].users.push(user)));
-
- res.stdRes.data = staff;
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+ })*/
+ .lean();
+
+ if (!users) {
+ throw {
+ code: 503,
+ message: "Unable to retrieve staff members",
+ };
+ }
+
+ const staff = {
+ atm: {
+ title: "Air Traffic Manager",
+ code: "atm",
+ email: "zab-atm",
+ users: [],
+ },
+ datm: {
+ title: "Deputy Air Traffic Manager",
+ code: "datm",
+ email: "zab-datm",
+ users: [],
+ },
+ ta: {
+ title: "Training Administrator",
+ code: "ta",
+ email: "zab-ta",
+ users: [],
+ },
+ ec: {
+ title: "Events Team",
+ code: "ec",
+ users: [],
+ },
+ wm: {
+ title: "Web Team",
+ code: "wm",
+ email: "john.morgan",
+ users: [],
+ },
+ fe: {
+ title: "Facility Engineer",
+ code: "fe",
+ email: "edward.sterling",
+ users: [],
+ },
+ ins: {
+ title: "Instructors",
+ code: "instructors",
+ users: [],
+ },
+ mtr: {
+ title: "Mentors",
+ code: "instructors",
+ users: [],
+ },
+ dta: {
+ title: "Deputy Training Administrator",
+ code: "dta",
+ email: "zab-dta",
+ users: [],
+ },
+ };
+
+ users.forEach((user) =>
+ user.roleCodes.forEach((role) => staff[role].users.push(user))
+ );
+
+ res.stdRes.data = staff;
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.get('/role', async (req, res) => {
- try {
- const roles = await Role.find().lean();
- res.stdRes.data = roles;
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
-});
+router.get("/role", async (req, res) => {
+ try {
+ const roles = await Role.find().lean();
+ res.stdRes.data = roles;
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
-router.get('/oi', async (req, res) => {
- try {
- const oi = await User.find({deletedAt: null, member: true}).select('oi').lean();
-
- if(!oi) {
- throw {
- code: 503,
- message: "Unable to retrieve operating initials"
- };
- }
-
- res.stdRes.data = oi.map(oi => oi.oi);
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+ return res.json(res.stdRes);
});
-router.get('/visit', getUser, auth(['atm', 'datm']), async ({res}) => {
- try {
- const applications = await VisitApplication.find({deletedAt: null, acceptedAt: null}).lean();
- res.stdRes.data = applications;
- } catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.get("/oi", async (req, res) => {
+ try {
+ const oi = await User.find({ deletedAt: null, member: true })
+ .select("oi")
+ .lean();
+
+ if (!oi) {
+ throw {
+ code: 503,
+ message: "Unable to retrieve operating initials",
+ };
+ }
+
+ res.stdRes.data = oi.map((oi) => oi.oi);
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.get('/absence', getUser, auth(['atm', 'datm']), async(req, res) => {
- try {
- const absences = await Absence.find({
- expirationDate: {
- $gte: new Date()
- },
- deleted: false
- }).populate(
- 'user', 'fname lname cid'
- ).sort({
- expirationDate: 'asc'
- }).lean();
-
- res.stdRes.data = absences;
- } catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.get("/visit", getUser, auth(["atm", "datm"]), async ({ res }) => {
+ try {
+ const applications = await VisitApplication.find({
+ deletedAt: null,
+ acceptedAt: null,
+ }).lean();
+ res.stdRes.data = applications;
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.post('/absence', getUser, auth(['atm', 'datm']), async(req, res) => {
- try {
- if(!req.body || req.body.controller === '' || req.body.expirationDate === 'T00:00:00.000Z' || req.body.reason === '') {
- throw {
- code: 400,
- message: "You must fill out all required fields"
- }
- }
-
- if(new Date(req.body.expirationDate) < new Date()) {
- throw {
- code: 400,
- message: "Expiration date must be in the future"
- }
- }
-
- await Absence.create(req.body);
-
- await Notification.create({
- recipient: req.body.controller,
- title: 'Leave of Absence granted',
- read: false,
- content: `You have been granted Leave of Absence until ${new Date(req.body.expirationDate).toLocaleString('en-US', {
- month: 'long',
- day: 'numeric',
- year: 'numeric',
- timeZone: 'UTC',
- })}.`
- });
-
- await req.app.dossier.create({
- by: res.user.cid,
- affected: req.body.controller,
- action: `%b added a leave of absence for %a: ${req.body.reason}`
- });
-
- } catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.get("/absence", getUser, auth(["atm", "datm"]), async (req, res) => {
+ try {
+ const absences = await Absence.find({
+ expirationDate: {
+ $gte: new Date(),
+ },
+ deleted: false,
+ })
+ .populate("user", "fname lname cid")
+ .sort({
+ expirationDate: "asc",
+ })
+ .lean();
+
+ res.stdRes.data = absences;
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.delete('/absence/:id', getUser, auth(['atm', 'datm']), async(req, res) => {
- try {
- if(!req.params.id) {
- throw {
- code: 401,
- message: "Invalid request"
- }
- }
-
- const absence = await Absence.findOne({_id: req.params.id});
- await absence.delete();
-
- await req.app.dossier.create({
- by: res.user.cid,
- affected: absence.controller,
- action: `%b deleted the leave of absence for %a.`
- });
- } catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.post("/absence", getUser, auth(["atm", "datm"]), async (req, res) => {
+ try {
+ if (
+ !req.body ||
+ req.body.controller === "" ||
+ req.body.expirationDate === "T00:00:00.000Z" ||
+ req.body.reason === ""
+ ) {
+ throw {
+ code: 400,
+ message: "You must fill out all required fields",
+ };
+ }
+
+ if (new Date(req.body.expirationDate) < new Date()) {
+ throw {
+ code: 400,
+ message: "Expiration date must be in the future",
+ };
+ }
+
+ await Absence.create(req.body);
+
+ await Notification.create({
+ recipient: req.body.controller,
+ title: "Leave of Absence granted",
+ read: false,
+ content: `You have been granted Leave of Absence until ${new Date(
+ req.body.expirationDate
+ ).toLocaleString("en-US", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ timeZone: "UTC",
+ })}.`,
+ });
+
+ await req.app.dossier.create({
+ by: res.user.cid,
+ affected: req.body.controller,
+ action: `%b added a leave of absence for %a: ${req.body.reason}`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.get('/log', getUser, auth(['atm', 'datm', 'ta', 'fe', 'ec', 'wm']), async (req, res) => {
- const page = +req.query.page || 1;
- const limit = +req.query.limit || 20;
- const amount = await req.app.dossier.countDocuments();
-
- try {
- const dossier = await req.app.dossier
- .find()
- .sort({
- createdAt: 'desc'
- }).skip(limit * (page - 1)).limit(limit).populate(
- 'userBy', 'fname lname cid'
- ).populate(
- 'userAffected', 'fname lname cid'
- ).lean();
-
- res.stdRes.data = {
- dossier,
- amount
- };
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
-})
-
-router.get('/:cid', getUser, async (req, res) => {
- try {
- const user = await User.findOne({
- cid: req.params.cid
- }).select(
- '-idsToken -discordInfo -trainingMilestones'
- ).populate('roles').populate('certifications').populate({
- path: 'absence',
- match: {
- expirationDate: {
- $gte: new Date()
- },
- deleted: false
- },
- select: '-reason'
- }).lean({virtuals: true});
-
- if(!user || [995625].includes(user.cid)) {
- throw {
- code: 503,
- message: "Unable to find controller"
- };
- }
-
- if(!res.user || !res.user.isStaff) {
- delete user.email;
- }
-
- res.stdRes.data = user;
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.delete(
+ "/absence/:id",
+ getUser,
+ auth(["atm", "datm"]),
+ async (req, res) => {
+ try {
+ if (!req.params.id) {
+ throw {
+ code: 401,
+ message: "Invalid request",
+ };
+ }
+
+ const absence = await Absence.findOne({ _id: req.params.id });
+ await absence.delete();
+
+ await req.app.dossier.create({
+ by: res.user.cid,
+ affected: absence.controller,
+ action: `%b deleted the leave of absence for %a.`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
+ }
+);
+
+router.get(
+ "/log",
+ getUser,
+ auth(["atm", "datm", "ta", "fe", "ec", "wm"]),
+ async (req, res) => {
+ const page = +req.query.page || 1;
+ const limit = +req.query.limit || 20;
+ const amount = await req.app.dossier.countDocuments();
+
+ try {
+ const dossier = await req.app.dossier
+ .find()
+ .sort({
+ createdAt: "desc",
+ })
+ .skip(limit * (page - 1))
+ .limit(limit)
+ .populate("userBy", "fname lname cid")
+ .populate("userAffected", "fname lname cid")
+ .lean();
+
+ res.stdRes.data = {
+ dossier,
+ amount,
+ };
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
+ }
+);
+
+router.get("/:cid", getUser, async (req, res) => {
+ try {
+ const user = await User.findOne({
+ cid: req.params.cid,
+ })
+ .select("-idsToken -discordInfo -trainingMilestones")
+ .populate("roles")
+ .populate("certifications")
+ .populate({
+ path: "absence",
+ match: {
+ expirationDate: {
+ $gte: new Date(),
+ },
+ deleted: false,
+ },
+ select: "-reason",
+ })
+ .lean({ virtuals: true });
+
+ if (!user || [995625].includes(user.cid)) {
+ throw {
+ code: 503,
+ message: "Unable to find controller",
+ };
+ }
+
+ if (!res.user || !res.user.isStaff) {
+ delete user.email;
+ }
+
+ res.stdRes.data = user;
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.get('/stats/:cid', async (req, res) => {
- try {
- const controllerHours = await ControllerHours.find({cid: req.params.cid});
- const hours = {
- gtyear: {
- del: 0,
- gnd: 0,
- twr: 0,
- app: 0,
- ctr: 0
- },
- total: {
- del: 0,
- gnd: 0,
- twr: 0,
- app: 0,
- ctr: 0
- },
- sessionCount: controllerHours.length,
- sessionAvg: 0,
- months: [],
- };
- const pos = {
- del: 'del',
- gnd: 'gnd',
- twr: 'twr',
- dep: 'app',
- app: 'app',
- ctr: 'ctr'
- }
- const today = L.utc();
-
- const getMonthYearString = date => date.toFormat('LLL yyyy');
-
- for(let i = 0; i < 13; i++) {
- const theMonth = today.minus({months: i});
- const ms = getMonthYearString(theMonth)
- hours[ms] = {
- del: 0,
- gnd: 0,
- twr: 0,
- app: 0,
- ctr: 0
- };
- hours.months.push(ms);
- }
-
- for(const sess of controllerHours) {
- const thePos = sess.position.toLowerCase().match(/([a-z]{3})$/); // 🤮
-
- if(thePos) {
- const start = L.fromJSDate(sess.timeStart).toUTC();
- const end = L.fromJSDate(sess.timeEnd).toUTC();
- const type = pos[thePos[1]];
- const length = end.toFormat('X') - start.toFormat('X');
- let ms = getMonthYearString(start);
-
- if(!hours[ms]) {
- ms = 'gtyear';
- }
-
- hours[ms][type] += length;
- hours.total[type] += length;
- }
-
- }
-
- hours.sessionAvg = Math.round(Object.values(hours.total).reduce((acc, cv) => acc + cv)/hours.sessionCount);
- res.stdRes.data = hours;
- }
-
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.get("/stats/:cid", async (req, res) => {
+ try {
+ const controllerHours = await ControllerHours.find({ cid: req.params.cid });
+ const hours = {
+ gtyear: {
+ del: 0,
+ gnd: 0,
+ twr: 0,
+ app: 0,
+ ctr: 0,
+ },
+ total: {
+ del: 0,
+ gnd: 0,
+ twr: 0,
+ app: 0,
+ ctr: 0,
+ },
+ sessionCount: controllerHours.length,
+ sessionAvg: 0,
+ months: [],
+ };
+ const pos = {
+ del: "del",
+ gnd: "gnd",
+ twr: "twr",
+ dep: "app",
+ app: "app",
+ ctr: "ctr",
+ };
+ const today = L.utc();
+
+ const getMonthYearString = (date) => date.toFormat("LLL yyyy");
+
+ for (let i = 0; i < 13; i++) {
+ const theMonth = today.minus({ months: i });
+ const ms = getMonthYearString(theMonth);
+ hours[ms] = {
+ del: 0,
+ gnd: 0,
+ twr: 0,
+ app: 0,
+ ctr: 0,
+ };
+ hours.months.push(ms);
+ }
+
+ for (const sess of controllerHours) {
+ const thePos = sess.position.toLowerCase().match(/([a-z]{3})$/); // 🤮
+
+ if (thePos) {
+ const start = L.fromJSDate(sess.timeStart).toUTC();
+ const end = L.fromJSDate(sess.timeEnd).toUTC();
+ const type = pos[thePos[1]];
+ const length = end.toFormat("X") - start.toFormat("X");
+ let ms = getMonthYearString(start);
+
+ if (!hours[ms]) {
+ ms = "gtyear";
+ }
+
+ hours[ms][type] += length;
+ hours.total[type] += length;
+ }
+ }
+
+ hours.sessionAvg = Math.round(
+ Object.values(hours.total).reduce((acc, cv) => acc + cv) /
+ hours.sessionCount
+ );
+ res.stdRes.data = hours;
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.post('/visit', getUser, async (req, res) => {
- try {
- if(!res.user) {
- throw {
- code: 401,
- message: "Unable to verify user"
- };
- }
-
- const userData = {
- cid: res.user.cid,
- fname: res.user.fname,
- lname: res.user.lname,
- rating: res.user.ratingLong,
- email: req.body.email,
- home: req.body.facility,
- reason: req.body.reason
- }
-
- await VisitApplication.create(userData);
-
- await transporter.sendMail({
- to: req.body.email,
- from: {
- name: "Albuquerque ARTCC",
- address: 'noreply@zabartcc.org'
- },
- subject: `Visiting Application Received | Albuquerque ARTCC`,
- template: 'visitReceived',
- context: {
- name: `${res.user.fname} ${res.user.lname}`,
- }
- });
- await transporter.sendMail({
- to: 'atm@zabartcc.org, datm@zabartcc.org',
- from: {
- name: "Albuquerque ARTCC",
- address: 'noreply@zabartcc.org'
- },
- subject: `New Visiting Application: ${res.user.fname} ${res.user.lname} | Albuquerque ARTCC`,
- template: 'staffNewVisit',
- context: {
- user: userData
- }
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.post("/visit", getUser, async (req, res) => {
+ try {
+ if (!res.user) {
+ throw {
+ code: 401,
+ message: "Unable to verify user",
+ };
+ }
+
+ const userData = {
+ cid: res.user.cid,
+ fname: res.user.fname,
+ lname: res.user.lname,
+ rating: res.user.ratingLong,
+ email: req.body.email,
+ home: req.body.facility,
+ reason: req.body.reason,
+ };
+
+ await VisitApplication.create(userData);
+
+ await transporter.sendMail({
+ to: req.body.email,
+ from: {
+ name: "Albuquerque ARTCC",
+ address: "noreply@zabartcc.org",
+ },
+ subject: `Visiting Application Received | Albuquerque ARTCC`,
+ template: "visitReceived",
+ context: {
+ name: `${res.user.fname} ${res.user.lname}`,
+ },
+ });
+ await transporter.sendMail({
+ to: "atm@zabartcc.org, datm@zabartcc.org",
+ from: {
+ name: "Albuquerque ARTCC",
+ address: "noreply@zabartcc.org",
+ },
+ subject: `New Visiting Application: ${res.user.fname} ${res.user.lname} | Albuquerque ARTCC`,
+ template: "staffNewVisit",
+ context: {
+ user: userData,
+ },
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.get('/visit/status', getUser, async (req, res) => {
- try {
- if(!res.user) {
- throw {
- code: 401,
- message: "Unable to verify user"
- };
- }
- const count = await VisitApplication.countDocuments({cid: res.user.cid, deleted: false});
- res.stdRes.data = count;
- } catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.get("/visit/status", getUser, async (req, res) => {
+ try {
+ if (!res.user) {
+ throw {
+ code: 401,
+ message: "Unable to verify user",
+ };
+ }
+ const count = await VisitApplication.countDocuments({
+ cid: res.user.cid,
+ deleted: false,
+ });
+ res.stdRes.data = count;
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.put('/visit/:cid', getUser, auth(['atm', 'datm']), async (req, res) => {
- try {
- await VisitApplication.delete({cid: req.params.cid});
-
- const user = await User.findOne({cid: req.params.cid});
- const oi = await User.find({deletedAt: null, member: true}).select('oi').lean();
- const userOi = generateOperatingInitials(user.fname, user.lname, oi.map(oi => oi.oi))
-
- user.member = true;
- user.vis = true;
- user.oi = userOi;
-
- await user.save();
-
- await transporter.sendMail({
- to: user.email,
- from: {
- name: "Albuquerque ARTCC",
- address: 'noreply@zabartcc.org'
- },
- subject: `Visiting Application Accepted | Albuquerque ARTCC`,
- template: 'visitAccepted',
- context: {
- name: `${user.fname} ${user.lname}`,
- }
- });
-
- await axios.post(`https://api.vatusa.net/v2/facility/ZAB/roster/manageVisitor/${req.params.cid}?apikey=${process.env.VATUSA_API_KEY}`)
-
- await req.app.dossier.create({
- by: res.user.cid,
- affected: user.cid,
- action: `%b approved the visiting application for %a.`
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.put("/visit/:cid", getUser, auth(["atm", "datm"]), async (req, res) => {
+ try {
+ await VisitApplication.delete({ cid: req.params.cid });
+
+ const user = await User.findOne({ cid: req.params.cid });
+ const oi = await User.find({ deletedAt: null, member: true })
+ .select("oi")
+ .lean();
+ const userOi = generateOperatingInitials(
+ user.fname,
+ user.lname,
+ oi.map((oi) => oi.oi)
+ );
+
+ user.member = true;
+ user.vis = true;
+ user.oi = userOi;
+
+ await user.save();
+
+ await transporter.sendMail({
+ to: user.email,
+ from: {
+ name: "Albuquerque ARTCC",
+ address: "noreply@zabartcc.org",
+ },
+ subject: `Visiting Application Accepted | Albuquerque ARTCC`,
+ template: "visitAccepted",
+ context: {
+ name: `${user.fname} ${user.lname}`,
+ },
+ });
+
+ await axios.post(
+ `https://api.vatusa.net/v2/facility/ZAB/roster/manageVisitor/${req.params.cid}?apikey=${process.env.VATUSA_API_KEY}`
+ );
+
+ await req.app.dossier.create({
+ by: res.user.cid,
+ affected: user.cid,
+ action: `%b approved the visiting application for %a.`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-
-router.delete('/visit/:cid', getUser, auth(['atm', 'datm']), async (req, res) => {
- try {
- await VisitApplication.delete({cid: req.params.cid});
-
- const user = await User.findOne({cid: req.params.cid});
-
- await transporter.sendMail({
- to: user.email,
- from: {
- name: "Albuquerque ARTCC",
- address: 'noreply@zabartcc.org'
- },
- subject: `Visiting Application Rejected | Albuquerque ARTCC`,
- template: 'visitRejected',
- context: {
- name: `${user.fname} ${user.lname}`,
- reason: req.body.reason
- }
- });
- await req.app.dossier.create({
- by: res.user.cid,
- affected: user.cid,
- action: `%b rejected the visiting application for %a: ${req.body.reason}`
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.delete(
+ "/visit/:cid",
+ getUser,
+ auth(["atm", "datm"]),
+ async (req, res) => {
+ try {
+ await VisitApplication.delete({ cid: req.params.cid });
+
+ const user = await User.findOne({ cid: req.params.cid });
+
+ await transporter.sendMail({
+ to: user.email,
+ from: {
+ name: "Albuquerque ARTCC",
+ address: "noreply@zabartcc.org",
+ },
+ subject: `Visiting Application Rejected | Albuquerque ARTCC`,
+ template: "visitRejected",
+ context: {
+ name: `${user.fname} ${user.lname}`,
+ reason: req.body.reason,
+ },
+ });
+ await req.app.dossier.create({
+ by: res.user.cid,
+ affected: user.cid,
+ action: `%b rejected the visiting application for %a: ${req.body.reason}`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
+ }
+);
+
+router.post("/:cid", microAuth, async (req, res) => {
+ try {
+ const user = await User.findOne({ cid: req.params.cid });
+ if (user) {
+ throw {
+ code: 409,
+ message: "This user already exists",
+ };
+ }
+
+ if (!req.body) {
+ throw {
+ code: 400,
+ message: "No user data provided",
+ };
+ }
+
+ const oi = await User.find({ deletedAt: null, member: true })
+ .select("oi")
+ .lean();
+ const userOi = generateOperatingInitials(
+ req.body.fname,
+ req.body.lname,
+ oi.map((oi) => oi.oi)
+ );
+ const { data } = await axios.get(
+ `https://ui-avatars.com/api/?name=${userOi}&size=256&background=122049&color=ffffff`,
+ { responseType: "arraybuffer" }
+ );
+
+ await req.app.s3
+ .putObject({
+ Bucket: "zabartcc/avatars",
+ Key: `${req.body.cid}-default.png`,
+ Body: data,
+ ContentType: "image/png",
+ ACL: "public-read",
+ ContentDisposition: "inline",
+ })
+ .promise();
+
+ await User.create({
+ ...req.body,
+ oi: userOi,
+ avatar: `${req.body.cid}-default.png`,
+ });
+
+ const ratings = [
+ "Unknown",
+ "OBS",
+ "S1",
+ "S2",
+ "S3",
+ "C1",
+ "C2",
+ "C3",
+ "I1",
+ "I2",
+ "I3",
+ "SUP",
+ "ADM",
+ ];
+
+ await transporter.sendMail({
+ to: "zab-atm@vatusa.net; zab-datm@vatusa.net; zab-ta@vatusa.net",
+ from: {
+ name: "Albuquerque ARTCC",
+ address: "noreply@zabartcc.org",
+ },
+ subject: `New ${req.body.vis ? "Visitor" : "Member"}: ${req.body.fname} ${
+ req.body.lname
+ } | Albuquerque ARTCC`,
+ template: "newController",
+ context: {
+ name: `${req.body.fname} ${req.body.lname}`,
+ email: req.body.email,
+ cid: req.body.cid,
+ rating: ratings[req.body.rating],
+ vis: req.body.vis,
+ type: req.body.vis ? "visitor" : "member",
+ home: req.body.vis ? req.body.homeFacility : "ZAB",
+ },
+ });
+
+ await req.app.dossier.create({
+ by: -1,
+ affected: req.body.cid,
+ action: `%a was created by an external service.`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.post('/:cid', microAuth, async (req, res) => {
- try {
- const user = await User.findOne({cid: req.params.cid});
- if(user) {
- throw {
- code: 409,
- message: "This user already exists"
- };
- }
-
- if(!req.body) {
- throw {
- code: 400,
- message: "No user data provided"
- };
- }
-
- const oi = await User.find({deletedAt: null, member: true}).select('oi').lean();
- const userOi = generateOperatingInitials(req.body.fname, req.body.lname, oi.map(oi => oi.oi))
- const {data} = await axios.get(`https://ui-avatars.com/api/?name=${userOi}&size=256&background=122049&color=ffffff`, {responseType: 'arraybuffer'});
-
- await req.app.s3.putObject({
- Bucket: 'zabartcc/avatars',
- Key: `${req.body.cid}-default.png`,
- Body: data,
- ContentType: 'image/png',
- ACL: 'public-read',
- ContentDisposition: 'inline',
- }).promise();
-
- await User.create({
- ...req.body,
- oi: userOi,
- avatar: `${req.body.cid}-default.png`,
- });
-
- const ratings = ['Unknown', 'OBS', 'S1', 'S2', 'S3', 'C1', 'C2', 'C3', 'I1', 'I2', 'I3', 'SUP', 'ADM'];
-
- await transporter.sendMail({
- to: "atm@zabartcc.org; datm@zabartcc.org; ta@zabartcc.org",
- from: {
- name: "Albuquerque ARTCC",
- address: 'noreply@zabartcc.org'
- },
- subject: `New ${req.body.vis ? 'Visitor' : 'Member'}: ${req.body.fname} ${req.body.lname} | Albuquerque ARTCC`,
- template: 'newController',
- context: {
- name: `${req.body.fname} ${req.body.lname}`,
- email: req.body.email,
- cid: req.body.cid,
- rating: ratings[req.body.rating],
- vis: req.body.vis,
- type: req.body.vis ? 'visitor' : 'member',
- home: req.body.vis ? req.body.homeFacility : 'ZAB'
- }
- });
-
- await req.app.dossier.create({
- by: -1,
- affected: req.body.cid,
- action: `%a was created by an external service.`
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.put("/:cid/member", microAuth, async (req, res) => {
+ try {
+ const user = await User.findOne({ cid: req.params.cid });
+
+ if (!user) {
+ throw {
+ code: 400,
+ message: "Unable to find user",
+ };
+ }
+
+ const oi = await User.find({ deletedAt: null, member: true })
+ .select("oi")
+ .lean();
+
+ user.member = req.body.member;
+ user.oi = req.body.member
+ ? generateOperatingInitials(
+ user.fname,
+ user.lname,
+ oi.map((oi) => oi.oi)
+ )
+ : null;
+ user.joinDate = req.body.member ? new Date() : null;
+
+ await user.save();
+
+ await req.app.dossier.create({
+ by: -1,
+ affected: req.params.cid,
+ action: `%a was ${
+ req.body.member ? "added to" : "removed from"
+ } the roster by an external service.`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.put('/:cid/member', microAuth, async (req, res) => {
- try {
- const user = await User.findOne({cid: req.params.cid});
-
- if(!user) {
- throw {
- code: 400,
- message: "Unable to find user"
- };
- }
-
- const oi = await User.find({deletedAt: null, member: true}).select('oi').lean();
-
- user.member = req.body.member;
- user.oi = (req.body.member) ? generateOperatingInitials(user.fname, user.lname, oi.map(oi => oi.oi)) : null;
- user.joinDate = req.body.member ? new Date() : null;
-
- await user.save();
-
-
- await req.app.dossier.create({
- by: -1,
- affected: req.params.cid,
- action: `%a was ${req.body.member ? 'added to' : 'removed from'} the roster by an external service.`
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
-})
-
-router.put('/:cid/visit', microAuth, async (req, res) => {
- try {
- const user = await User.findOne({cid: req.params.cid});
-
- if(!user) {
- throw {
- code: 400,
- message: "Unable to find user"
- };
- }
-
- user.vis = req.body.vis;
- user.joinDate = new Date();
-
- await user.save();
-
- await req.app.dossier.create({
- by: -1,
- affected: req.params.cid,
- action: `%a was set as a ${req.body.vis ? 'visiting controller' : 'home controller'} by an external service.`
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
-})
-
-router.put('/:cid', getUser, auth(['atm', 'datm', 'ta', 'wm', 'ins']), async (req, res) => {
- try {
- if(!req.body.form) {
- throw {
- code: 400,
- message: "No user data included"
- };
- }
-
- const {fname, lname, email, oi, roles, certs, vis} = req.body.form;
- const toApply = {
- roles: [],
- certifications: []
- };
-
- for(const [code, set] of Object.entries(roles)) {
- if(set) {
- toApply.roles.push(code);
- }
- }
-
- for(const [code, set] of Object.entries(certs)) {
- if(set) {
- toApply.certifications.push(code);
- }
- }
-
- const {data} = await axios.get(`https://ui-avatars.com/api/?name=${oi}&size=256&background=122049&color=ffffff`, {responseType: 'arraybuffer'});
-
- await req.app.s3.putObject({
- Bucket: 'zabartcc/avatars',
- Key: `${req.params.cid}-default.png`,
- Body: data,
- ContentType: 'image/png',
- ACL: 'public-read',
- ContentDisposition: 'inline',
- }).promise();
-
- await User.findOneAndUpdate({cid: req.params.cid}, {
- fname,
- lname,
- email,
- oi,
- vis,
- roleCodes: toApply.roles,
- certCodes: toApply.certifications,
- });
-
- await req.app.dossier.create({
- by: res.user.cid,
- affected: req.params.cid,
- action: `%a was updated by %b.`
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.put("/:cid/visit", microAuth, async (req, res) => {
+ try {
+ const user = await User.findOne({ cid: req.params.cid });
+
+ if (!user) {
+ throw {
+ code: 400,
+ message: "Unable to find user",
+ };
+ }
+
+ user.vis = req.body.vis;
+ user.joinDate = new Date();
+
+ await user.save();
+
+ await req.app.dossier.create({
+ by: -1,
+ affected: req.params.cid,
+ action: `%a was set as a ${
+ req.body.vis ? "visiting controller" : "home controller"
+ } by an external service.`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
-router.delete('/:cid', getUser, auth(['atm', 'datm']), async (req, res) => {
- try {
- if(!req.body.reason) {
- throw {
- code: 400,
- message: "You must specify a reason"
- };
- }
-
- const user = await User.findOneAndUpdate({cid: req.params.cid}, {
- member: false
- });
-
- if(user.vis) {
- await axios.delete(`https://api.vatusa.net/v2/facility/ZAB/roster/manageVisitor/${req.params.cid}`, {
- params: {
- apikey: process.env.VATUSA_API_KEY,
- },
- data: {
- reason: req.body.reason
- }
- });
- } else {
- await axios.delete(`https://api.vatusa.net/v2/facility/ZAB/roster/${req.params.cid}`, {
- params: {
- apikey: process.env.VATUSA_API_KEY,
- },
- data: {
- reason: req.body.reason
- }
- });
- }
-
- await req.app.dossier.create({
- by: res.user.cid,
- affected: req.params.cid,
- action: `%a was removed from the roster by %b: ${req.body.reason}`
- });
- }
- catch(e) {
- req.app.Sentry.captureException(e);
- res.stdRes.ret_det = e;
- }
-
- return res.json(res.stdRes);
+router.put(
+ "/:cid",
+ getUser,
+ auth(["atm", "datm", "ta", "wm", "ins"]),
+ async (req, res) => {
+ try {
+ if (!req.body.form) {
+ throw {
+ code: 400,
+ message: "No user data included",
+ };
+ }
+
+ const { fname, lname, email, oi, roles, certs, vis } = req.body.form;
+ const toApply = {
+ roles: [],
+ certifications: [],
+ };
+
+ for (const [code, set] of Object.entries(roles)) {
+ if (set) {
+ toApply.roles.push(code);
+ }
+ }
+
+ for (const [code, set] of Object.entries(certs)) {
+ if (set) {
+ toApply.certifications.push(code);
+ }
+ }
+
+ const { data } = await axios.get(
+ `https://ui-avatars.com/api/?name=${oi}&size=256&background=122049&color=ffffff`,
+ { responseType: "arraybuffer" }
+ );
+
+ await req.app.s3
+ .putObject({
+ Bucket: "zabartcc/avatars",
+ Key: `${req.params.cid}-default.png`,
+ Body: data,
+ ContentType: "image/png",
+ ACL: "public-read",
+ ContentDisposition: "inline",
+ })
+ .promise();
+
+ await User.findOneAndUpdate(
+ { cid: req.params.cid },
+ {
+ fname,
+ lname,
+ email,
+ oi,
+ vis,
+ roleCodes: toApply.roles,
+ certCodes: toApply.certifications,
+ }
+ );
+
+ await req.app.dossier.create({
+ by: res.user.cid,
+ affected: req.params.cid,
+ action: `%a was updated by %b.`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
+ }
+);
+
+router.delete("/:cid", getUser, auth(["atm", "datm"]), async (req, res) => {
+ try {
+ if (!req.body.reason) {
+ throw {
+ code: 400,
+ message: "You must specify a reason",
+ };
+ }
+
+ const user = await User.findOneAndUpdate(
+ { cid: req.params.cid },
+ {
+ member: false,
+ }
+ );
+
+ if (user.vis) {
+ await axios.delete(
+ `https://api.vatusa.net/v2/facility/ZAB/roster/manageVisitor/${req.params.cid}`,
+ {
+ params: {
+ apikey: process.env.VATUSA_API_KEY,
+ },
+ data: {
+ reason: req.body.reason,
+ },
+ }
+ );
+ } else {
+ await axios.delete(
+ `https://api.vatusa.net/v2/facility/ZAB/roster/${req.params.cid}`,
+ {
+ params: {
+ apikey: process.env.VATUSA_API_KEY,
+ },
+ data: {
+ reason: req.body.reason,
+ },
+ }
+ );
+ }
+
+ await req.app.dossier.create({
+ by: res.user.cid,
+ affected: req.params.cid,
+ action: `%a was removed from the roster by %b: ${req.body.reason}`,
+ });
+ } catch (e) {
+ req.app.Sentry.captureException(e);
+ res.stdRes.ret_det = e;
+ }
+
+ return res.json(res.stdRes);
});
/**
@@ -848,46 +958,50 @@ router.delete('/:cid', getUser, auth(['atm', 'datm']), async (req, res) => {
* @return A two character set of operating initials (e.g. RA).
*/
const generateOperatingInitials = (fname, lname, usedOi) => {
- let operatingInitials;
- const MAX_TRIES = 10;
-
- operatingInitials = `${fname.charAt(0).toUpperCase()}${lname.charAt(0).toUpperCase()}`;
-
- if(!usedOi.includes(operatingInitials)) {
- return operatingInitials;
- }
-
- operatingInitials = `${lname.charAt(0).toUpperCase()}${fname.charAt(0).toUpperCase()}`;
-
- if(!usedOi.includes(operatingInitials)) {
- return operatingInitials;
- }
-
- const chars = `${lname.toUpperCase()}${fname.toUpperCase()}`;
-
- let tries = 0;
-
- do {
- operatingInitials = random(chars, 2);
- tries++;
- } while(usedOi.includes(operatingInitials) || tries > MAX_TRIES);
-
- if(!usedOi.includes(operatingInitials)) {
- return operatingInitials;
- }
-
- tries = 0;
-
- do {
- operatingInitials = random('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 2);
- tries++;
- } while(usedOi.includes(operatingInitials) || tries > MAX_TRIES);
-
- if(!usedOi.includes(operatingInitials)) {
- return operatingInitials;
- }
-
- return false;
+ let operatingInitials;
+ const MAX_TRIES = 10;
+
+ operatingInitials = `${fname.charAt(0).toUpperCase()}${lname
+ .charAt(0)
+ .toUpperCase()}`;
+
+ if (!usedOi.includes(operatingInitials)) {
+ return operatingInitials;
+ }
+
+ operatingInitials = `${lname.charAt(0).toUpperCase()}${fname
+ .charAt(0)
+ .toUpperCase()}`;
+
+ if (!usedOi.includes(operatingInitials)) {
+ return operatingInitials;
+ }
+
+ const chars = `${lname.toUpperCase()}${fname.toUpperCase()}`;
+
+ let tries = 0;
+
+ do {
+ operatingInitials = random(chars, 2);
+ tries++;
+ } while (usedOi.includes(operatingInitials) || tries > MAX_TRIES);
+
+ if (!usedOi.includes(operatingInitials)) {
+ return operatingInitials;
+ }
+
+ tries = 0;
+
+ do {
+ operatingInitials = random("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 2);
+ tries++;
+ } while (usedOi.includes(operatingInitials) || tries > MAX_TRIES);
+
+ if (!usedOi.includes(operatingInitials)) {
+ return operatingInitials;
+ }
+
+ return false;
};
/**
@@ -897,11 +1011,11 @@ const generateOperatingInitials = (fname, lname, usedOi) => {
* @return String of selected characters.
*/
const random = (str, len) => {
- let ret = '';
- for (let i = 0; i < len; i++) {
- ret = `${ret}${str.charAt(Math.floor(Math.random() * str.length))}`;
- }
- return ret;
+ let ret = "";
+ for (let i = 0; i < len; i++) {
+ ret = `${ret}${str.charAt(Math.floor(Math.random() * str.length))}`;
+ }
+ return ret;
};
-export default router;
\ No newline at end of file
+export default router;
diff --git a/helpers/controllerActivityHelper.js b/helpers/controllerActivityHelper.js
index bb4508c..7b91b7d 100644
--- a/helpers/controllerActivityHelper.js
+++ b/helpers/controllerActivityHelper.js
@@ -1,12 +1,12 @@
-import cron from 'node-cron';
-import transporter from '../config/mailer.js';
-import User from '../models/User.js';
-import ControllerHours from '../models/ControllerHours.js';
-import TrainingRequest from '../models/TrainingRequest.js';
-import { DateTime as luxon } from 'luxon'
-import Redis from 'redis';
-import RedisLock from 'redis-lock';
-import env from 'dotenv';
+import cron from "node-cron";
+import transporter from "../config/mailer.js";
+import User from "../models/User.js";
+import ControllerHours from "../models/ControllerHours.js";
+import TrainingRequest from "../models/TrainingRequest.js";
+import { DateTime as luxon } from "luxon";
+import Redis from "redis";
+import RedisLock from "redis-lock";
+import env from "dotenv";
env.config();
@@ -15,162 +15,180 @@ let redisLock = RedisLock(redis);
await redis.connect();
const observerRatingCode = 1;
-const activityWindowInDays = 90;
+const activityWindowInDays = 90; // Update from 60 to 90 days
const gracePeriodInDays = 15;
-const requiredHoursPerPeriod = 2;
+const requiredHoursPerPeriod = 3; // Ensure this is set to 3 hours
const redisActivityCheckKey = "ACTIVITYCHECKRUNNING";
+// Helper function to get the start and end dates of the current quarter
+function getCurrentQuarterDates() {
+ const now = luxon.utc();
+ const startOfQuarter = now.startOf("quarter");
+ const endOfQuarter = now.endOf("quarter");
+ return { startOfQuarter, endOfQuarter };
+}
+
/**
* Registers a CRON job that sends controllers reminder emails.
*/
function registerControllerActivityChecking() {
- try {
- if (process.env.NODE_ENV === 'prod') {
- cron.schedule('0 0 * * *', async () => {
- // Lock the activity check to avoid multiple app instances trying to simulatenously run the check.
- const lockRunningActivityCheck = await redisLock(redisActivityCheckKey);
+ try {
+ if (process.env.NODE_ENV === "prod") {
+ cron.schedule("0 0 * * *", async () => {
+ // Lock the activity check to avoid multiple app instances trying to simulatenously run the check.
+ const lockRunningActivityCheck = await redisLock(redisActivityCheckKey);
- await checkControllerActivity();
- await checkControllersNeedingRemoval();
+ await checkControllerActivity();
+ await checkControllersNeedingRemoval();
- lockRunningActivityCheck(); // Releases the lock.
- });
+ lockRunningActivityCheck(); // Releases the lock.
+ });
- console.log("Successfully registered activity CRON checks")
- }
- }
- catch (e) {
- console.log("Error registering activity CRON checks")
- console.error(e)
+ console.log("Successfully registered activity CRON checks");
}
+ } catch (e) {
+ console.log("Error registering activity CRON checks");
+ console.error(e);
+ }
}
/**
* Checks controllers for activity and sends a reminder email.
*/
async function checkControllerActivity() {
- const today = luxon.utc();
- const minActivityDate = today.minus({ days: activityWindowInDays - 1 });
+ const today = luxon.utc();
+ const minActivityDate = today.minus({ days: activityWindowInDays - 1 });
- try {
- const usersNeedingActivityCheck = await User.find(
- {
- member: true,
- $or: [{ nextActivityCheckDate: { $lte: today } }, { nextActivityCheckDate: null }]
- });
+ try {
+ const usersNeedingActivityCheck = await User.find({
+ member: true,
+ $or: [
+ { nextActivityCheckDate: { $lte: today } },
+ { nextActivityCheckDate: null },
+ ],
+ });
- await User.updateMany(
- { "cid": { $in: usersNeedingActivityCheck.map(u => u.cid) } },
- {
- nextActivityCheckDate: today.plus({ days: activityWindowInDays })
- }
- )
+ await User.updateMany(
+ { cid: { $in: usersNeedingActivityCheck.map((u) => u.cid) } },
+ {
+ nextActivityCheckDate: today.plus({ days: activityWindowInDays }),
+ }
+ );
- const inactiveUserData = await getControllerInactivityData(usersNeedingActivityCheck, minActivityDate);
+ const inactiveUserData = await getControllerInactivityData(
+ usersNeedingActivityCheck,
+ minActivityDate
+ );
- inactiveUserData.forEach(async record => {
- await User.updateOne(
- { "cid": record.user.cid },
- {
- removalWarningDeliveryDate: today.plus({ days: gracePeriodInDays })
- }
- )
+ inactiveUserData.forEach(async (record) => {
+ await User.updateOne(
+ { cid: record.user.cid },
+ {
+ removalWarningDeliveryDate: today.plus({ days: gracePeriodInDays }),
+ }
+ );
- transporter.sendMail({
- to: record.user.email,
- from: {
- name: "Albuquerque ARTCC",
- address: 'noreply@zabartcc.org'
- },
- subject: `Controller Activity Warning | Albuquerque ARTCC`,
- template: 'activityReminder',
- context: {
- name: record.user.fname,
- requiredHours: requiredHoursPerPeriod,
- activityWindow: activityWindowInDays,
- daysRemaining: gracePeriodInDays,
- currentHours: record.hours.toFixed(2)
- }
- });
- });
- }
- catch (e) {
- console.error(e)
- }
+ transporter.sendMail({
+ to: record.user.email,
+ from: {
+ name: "Albuquerque ARTCC",
+ address: "noreply@zabartcc.org",
+ },
+ subject: `Controller Activity Warning | Albuquerque ARTCC`,
+ template: "activityReminder",
+ context: {
+ name: record.user.fname,
+ requiredHours: requiredHoursPerPeriod,
+ activityWindow: activityWindowInDays,
+ daysRemaining: gracePeriodInDays,
+ currentHours: record.hours.toFixed(2),
+ },
+ });
+ });
+ } catch (e) {
+ console.error(e);
+ }
}
-
/**
* Checks for controllers that did not maintain activity and sends a removal email.
*/
async function checkControllersNeedingRemoval() {
- const today = luxon.utc();
+ const { startOfQuarter, endOfQuarter } = getCurrentQuarterDates();
- try {
- const usersNeedingRemovalWarningCheck = await User.find(
- {
- member: true,
- removalWarningDeliveryDate: { $lte: today }
- });
+ try {
+ const usersNeedingRemovalWarningCheck = await User.find({
+ member: true,
+ removalWarningDeliveryDate: { $lte: endOfQuarter },
+ });
- usersNeedingRemovalWarningCheck.forEach(async user => {
- const minActivityDate = luxon.fromJSDate(user.removalWarningDeliveryDate).minus({ days: activityWindowInDays - 1 });
- const userHourSums = await ControllerHours.aggregate([
- {
- $match: {
- timeStart: { $gt: minActivityDate },
- cid: user.cid
- }
- },
- {
- $project: {
- length: {
- "$divide": [
- { $subtract: ['$timeEnd', '$timeStart'] },
- 60 * 1000 * 60 // Convert to hours.
- ]
- }
- }
- },
- {
- $group: {
- _id: "$cid",
- total: { "$sum": "$length" }
- }
- }
- ]);
- const userTotalHoursInPeriod = (userHourSums && userHourSums.length > 0) ? userHourSums[0].total : 0;
- const userTrainingRequestCount = await TrainingRequest.count({ studentCid: user.cid, startTime: { $gt: minActivityDate } });
+ usersNeedingRemovalWarningCheck.forEach(async (user) => {
+ const userHourSums = await ControllerHours.aggregate([
+ {
+ $match: {
+ timeStart: { $gt: startOfQuarter },
+ cid: user.cid,
+ },
+ },
+ {
+ $project: {
+ length: {
+ $divide: [
+ { $subtract: ["$timeEnd", "$timeStart"] },
+ 60 * 1000 * 60, // Convert to hours.
+ ],
+ },
+ },
+ },
+ {
+ $group: {
+ _id: "$cid",
+ total: { $sum: "$length" },
+ },
+ },
+ ]);
+ const userTotalHoursInPeriod =
+ userHourSums && userHourSums.length > 0 ? userHourSums[0].total : 0;
+ const userTrainingRequestCount = await TrainingRequest.count({
+ studentCid: user.cid,
+ startTime: { $gt: startOfQuarter },
+ });
- await User.updateOne(
- { "cid": user.cid },
- {
- removalWarningDeliveryDate: null
- }
- )
+ await User.updateOne(
+ { cid: user.cid },
+ {
+ removalWarningDeliveryDate: null,
+ }
+ );
- if (controllerIsInactive(user, userTotalHoursInPeriod, userTrainingRequestCount, minActivityDate)) {
- transporter.sendMail({
- to: user.email,
- cc: 'datm@zabartcc.org',
- from: {
- name: "Albuquerque ARTCC",
- address: 'noreply@zabartcc.org'
- },
- subject: `Controller Inactivity Notice | Albuquerque ARTCC`,
- template: 'activityWarning',
- context: {
- name: user.fname,
- requiredHours: requiredHoursPerPeriod,
- activityWindow: activityWindowInDays
- }
- });
- }
+ if (
+ controllerIsInactive(
+ user,
+ userTotalHoursInPeriod,
+ userTrainingRequestCount,
+ startOfQuarter
+ )
+ ) {
+ transporter.sendMail({
+ to: user.email,
+ cc: "zab-datm@vatusa.net",
+ from: {
+ name: "Albuquerque ARTCC",
+ address: "noreply@zabartcc.org",
+ },
+ subject: `Controller Inactivity Notice | Albuquerque ARTCC`,
+ template: "activityWarning",
+ context: {
+ name: user.fname,
+ requiredHours: requiredHoursPerPeriod,
+ activityWindow: activityWindowInDays,
+ },
});
- }
- catch (e) {
- console.error(e);
- }
+ }
+ });
+ } catch (e) {
+ console.error(e);
+ }
}
/**
@@ -179,63 +197,85 @@ async function checkControllersNeedingRemoval() {
* @param minActivityDate The start date of the activity period.
* @return A map of inactive controllers with the amount of hours they've controlled in the current period.
*/
-async function getControllerInactivityData(controllersToGetStatusFor, minActivityDate) {
- const controllerHoursSummary = {};
- const controllerTrainingSummary = {};
- const inactiveControllers = [];
- const controllerCids = controllersToGetStatusFor.map(c => c.cid);
+async function getControllerInactivityData(
+ controllersToGetStatusFor,
+ minActivityDate
+) {
+ const controllerHoursSummary = {};
+ const controllerTrainingSummary = {};
+ const inactiveControllers = [];
+ const controllerCids = controllersToGetStatusFor.map((c) => c.cid);
- (await ControllerHours.aggregate([
- {
- $match: {
- timeStart: { $gt: minActivityDate },
- cid: { $in: controllerCids }
- }
+ (
+ await ControllerHours.aggregate([
+ {
+ $match: {
+ timeStart: { $gt: minActivityDate },
+ cid: { $in: controllerCids },
},
- {
- $project: {
- length: {
- "$divide": [
- { $subtract: ['$timeEnd', '$timeStart'] },
- 60 * 1000 * 60 // Convert to hours.
- ]
- },
- cid: 1
- }
+ },
+ {
+ $project: {
+ length: {
+ $divide: [
+ { $subtract: ["$timeEnd", "$timeStart"] },
+ 60 * 1000 * 60, // Convert to hours.
+ ],
+ },
+ cid: 1,
},
- {
- $group: {
- _id: "$cid",
- total: { "$sum": "$length" }
- }
- }
- ])).forEach(i => controllerHoursSummary[i._id] = i.total);
+ },
+ {
+ $group: {
+ _id: "$cid",
+ total: { $sum: "$length" },
+ },
+ },
+ ])
+ ).forEach((i) => (controllerHoursSummary[i._id] = i.total));
- (await TrainingRequest.aggregate([
- { $match: { startTime: { $gt: minActivityDate }, studentCid: { $in: controllerCids } } },
- {
- $group: {
- _id: "$studentCid",
- total: { $sum: 1 }
- }
- }
- ])).forEach(i => controllerTrainingSummary[i._id] = i.total);
+ (
+ await TrainingRequest.aggregate([
+ {
+ $match: {
+ startTime: { $gt: minActivityDate },
+ studentCid: { $in: controllerCids },
+ },
+ },
+ {
+ $group: {
+ _id: "$studentCid",
+ total: { $sum: 1 },
+ },
+ },
+ ])
+ ).forEach((i) => (controllerTrainingSummary[i._id] = i.total));
- controllersToGetStatusFor.forEach(async user => {
- let controllerHoursCount = controllerHoursSummary[user.cid] ?? 0;
- let controllerTrainingSessions = controllerTrainingSummary[user.cid] != null ? controllerTrainingSummary[user.cid].length : 0
+ controllersToGetStatusFor.forEach(async (user) => {
+ let controllerHoursCount = controllerHoursSummary[user.cid] ?? 0;
+ let controllerTrainingSessions =
+ controllerTrainingSummary[user.cid] != null
+ ? controllerTrainingSummary[user.cid].length
+ : 0;
- if (controllerIsInactive(user, controllerHoursCount, controllerTrainingSessions, minActivityDate)) {
- const inactiveControllerData = {
- user: user,
- hours: controllerHoursCount
- };
+ if (
+ controllerIsInactive(
+ user,
+ controllerHoursCount,
+ controllerTrainingSessions,
+ minActivityDate
+ )
+ ) {
+ const inactiveControllerData = {
+ user: user,
+ hours: controllerHoursCount,
+ };
- inactiveControllers.push(inactiveControllerData);
- }
- });
+ inactiveControllers.push(inactiveControllerData);
+ }
+ });
- return inactiveControllers;
+ return inactiveControllers;
}
/**
@@ -246,14 +286,26 @@ async function getControllerInactivityData(controllersToGetStatusFor, minActivit
* @param minActivityDate The start date of the activity period.
* @return True if controller is inactive, false otherwise.
*/
-function controllerIsInactive(user, hoursInPeriod, trainingSessionInPeriod, minActivityDate) {
- const controllerHasLessThanTwoHours = (hoursInPeriod ?? 0) < requiredHoursPerPeriod;
- const controllerJoinedMoreThan60DaysAgo = (user.joinDate ?? user.createdAt) < minActivityDate;
- const controllerIsNotObserverWithTrainingSession = user.rating != observerRatingCode || trainingSessionInPeriod < 1;
+function controllerIsInactive(
+ user,
+ hoursInPeriod,
+ trainingSessionInPeriod,
+ minActivityDate
+) {
+ const controllerHasLessThanRequiredHours =
+ (hoursInPeriod ?? 0) < requiredHoursPerPeriod;
+ const controllerJoinedMoreThanActivityWindowAgo =
+ (user.joinDate ?? user.createdAt) < minActivityDate;
+ const controllerIsNotObserverWithTrainingSession =
+ user.rating != observerRatingCode || trainingSessionInPeriod < 1;
- return controllerHasLessThanTwoHours && controllerJoinedMoreThan60DaysAgo && controllerIsNotObserverWithTrainingSession;
+ return (
+ controllerHasLessThanRequiredHours &&
+ controllerJoinedMoreThanActivityWindowAgo &&
+ controllerIsNotObserverWithTrainingSession
+ );
}
export default {
- registerControllerActivityChecking: registerControllerActivityChecking
-}
+ registerControllerActivityChecking: registerControllerActivityChecking,
+};