From f0f9c6dfe5b8d1a332a199a7a87e37d3331f7ffa Mon Sep 17 00:00:00 2001 From: Dan Gowans Date: Thu, 15 Sep 2022 16:11:57 -0400 Subject: [PATCH] outlook integration --- app.js | 3 + app.ts | 4 + handlers/api-get/milestoneICS.d.ts | 3 + handlers/api-get/milestoneICS.js | 56 ++++++++++++ handlers/api-get/milestoneICS.ts | 88 ++++++++++++++++++ handlers/permissions.d.ts | 1 + handlers/permissions.js | 6 ++ handlers/permissions.ts | 8 ++ handlers/workOrders-get/outlook.d.ts | 3 + handlers/workOrders-get/outlook.js | 11 +++ handlers/workOrders-get/outlook.ts | 16 ++++ helpers/functions.api.d.ts | 1 + helpers/functions.api.js | 10 +++ helpers/functions.api.ts | 12 +++ helpers/functions.user.d.ts | 1 + helpers/functions.user.js | 18 ++++ helpers/functions.user.ts | 25 ++++++ .../getWorkOrderMilestones.d.ts | 2 + .../lotOccupancyDB/getWorkOrderMilestones.js | 17 +++- .../lotOccupancyDB/getWorkOrderMilestones.ts | 26 +++++- package-lock.json | 82 +++++++++++++++-- package.json | 1 + public-typescript/workOrderOutlook.d.ts | 0 public-typescript/workOrderOutlook.js | 49 ++++++++++ public-typescript/workOrderOutlook.ts | 89 +++++++++++++++++++ public/javascripts/workOrderOutlook.min.js | 1 + routes/api.d.ts | 2 + routes/api.js | 5 ++ routes/api.ts | 9 ++ routes/workOrders.js | 2 + routes/workOrders.ts | 6 ++ views/_header.ejs | 6 +- views/_menu-workOrders.ejs | 11 +++ views/workOrder-outlook.ejs | 86 ++++++++++++++++++ 34 files changed, 650 insertions(+), 10 deletions(-) create mode 100644 handlers/api-get/milestoneICS.d.ts create mode 100644 handlers/api-get/milestoneICS.js create mode 100644 handlers/api-get/milestoneICS.ts create mode 100644 handlers/workOrders-get/outlook.d.ts create mode 100644 handlers/workOrders-get/outlook.js create mode 100644 handlers/workOrders-get/outlook.ts create mode 100644 public-typescript/workOrderOutlook.d.ts create mode 100644 public-typescript/workOrderOutlook.js create mode 100644 public-typescript/workOrderOutlook.ts create mode 100644 public/javascripts/workOrderOutlook.min.js create mode 100644 routes/api.d.ts create mode 100644 routes/api.js create mode 100644 routes/api.ts create mode 100644 views/workOrder-outlook.ejs diff --git a/app.js b/app.js index 55222a44..c7995842 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,7 @@ import session from "express-session"; import FileStore from "session-file-store"; import routerLogin from "./routes/login.js"; import routerDashboard from "./routes/dashboard.js"; +import routerApi from "./routes/api.js"; import routerLots from "./routes/lots.js"; import routerMaps from "./routes/maps.js"; import routerLotOccupancies from "./routes/lotOccupancies.js"; @@ -22,6 +23,7 @@ import * as htmlFns from "@cityssm/expressjs-server-js/htmlFns.js"; import { version } from "./version.js"; import * as databaseInitializer from "./helpers/initializer.database.js"; import debug from "debug"; +import { apiGetHandler } from "./handlers/permissions.js"; const debugApp = debug("lot-occupancy-system:app"); databaseInitializer.initializeDatabase(); const __dirname = "."; @@ -106,6 +108,7 @@ app.get(urlPrefix + "/", sessionChecker, (_request, response) => { response.redirect(urlPrefix + "/dashboard"); }); app.use(urlPrefix + "/dashboard", sessionChecker, routerDashboard); +app.use(urlPrefix + "/api/:apiKey", apiGetHandler, routerApi); app.use(urlPrefix + "/lots", sessionChecker, routerLots); app.use(urlPrefix + "/maps", sessionChecker, routerMaps); app.use(urlPrefix + "/lotOccupancies", sessionChecker, routerLotOccupancies); diff --git a/app.ts b/app.ts index b5e643ad..0e89064e 100644 --- a/app.ts +++ b/app.ts @@ -12,6 +12,7 @@ import FileStore from "session-file-store"; import routerLogin from "./routes/login.js"; import routerDashboard from "./routes/dashboard.js"; +import routerApi from "./routes/api.js"; import routerLots from "./routes/lots.js"; import routerMaps from "./routes/maps.js"; import routerLotOccupancies from "./routes/lotOccupancies.js"; @@ -29,6 +30,7 @@ import { version } from "./version.js"; import * as databaseInitializer from "./helpers/initializer.database.js"; import debug from "debug"; +import { apiGetHandler } from "./handlers/permissions.js"; const debugApp = debug("lot-occupancy-system:app"); /* @@ -210,6 +212,8 @@ app.get(urlPrefix + "/", sessionChecker, (_request, response) => { app.use(urlPrefix + "/dashboard", sessionChecker, routerDashboard); +app.use(urlPrefix + "/api/:apiKey", apiGetHandler, routerApi); + app.use(urlPrefix + "/lots", sessionChecker, routerLots); app.use(urlPrefix + "/maps", sessionChecker, routerMaps); app.use(urlPrefix + "/lotOccupancies", sessionChecker, routerLotOccupancies); diff --git a/handlers/api-get/milestoneICS.d.ts b/handlers/api-get/milestoneICS.d.ts new file mode 100644 index 00000000..9621c611 --- /dev/null +++ b/handlers/api-get/milestoneICS.d.ts @@ -0,0 +1,3 @@ +import type { RequestHandler } from "express"; +export declare const handler: RequestHandler; +export default handler; diff --git a/handlers/api-get/milestoneICS.js b/handlers/api-get/milestoneICS.js new file mode 100644 index 00000000..8c009471 --- /dev/null +++ b/handlers/api-get/milestoneICS.js @@ -0,0 +1,56 @@ +import ical, { ICalEventStatus } from "ical-generator"; +import { getWorkOrderMilestones } from "../../helpers/lotOccupancyDB/getWorkOrderMilestones.js"; +const timeStringSplitRegex = /[ :-]/; +export const handler = (request, response) => { + const workOrderMilestones = getWorkOrderMilestones({ + workOrderMilestoneDateFilter: "recent", + workOrderTypeIds: request.query.workOrderTypeIds, + workOrderMilestoneTypeIds: request.query + .workOrderMilestoneTypeIds + }, { includeWorkOrders: true, orderBy: "date" }); + const calendar = ical({ + name: "Work Order Milestone Calendar" + }); + for (const milestone of workOrderMilestones) { + const milestoneTimePieces = (milestone.workOrderMilestoneDateString + + " " + + milestone.workOrderMilestoneTimeString).split(timeStringSplitRegex); + const milestoneDate = new Date(Number.parseInt(milestoneTimePieces[0], 10), Number.parseInt(milestoneTimePieces[1], 10) - 1, Number.parseInt(milestoneTimePieces[2], 10), Number.parseInt(milestoneTimePieces[3], 10), Number.parseInt(milestoneTimePieces[4], 10)); + const eventData = { + start: milestoneDate, + stamp: new Date(milestone.recordCreate_timeMillis), + lastModified: new Date(milestone.recordUpdate_timeMillis), + allDay: !milestone.workOrderMilestoneTime, + summary: milestone.workOrderMilestoneDescription + }; + const calendarEvent = calendar.createEvent(eventData); + if (milestone.workOrderMilestoneCompletionDate) { + calendarEvent.status(ICalEventStatus.CONFIRMED); + } + if (milestone.workOrderMilestoneTypeId) { + calendarEvent.createCategory({ + name: milestone.workOrderMilestoneType + }); + } + if (milestone.workOrder.workOrderLots.length > 0) { + const lotNames = []; + for (const lot of milestone.workOrder.workOrderLots) { + lotNames.push(lot.mapName + ": " + lot.lotName); + } + calendarEvent.location(lotNames.join(", ")); + } + if (milestone.workOrder.workOrderLotOccupancies.length > 0) { + for (const lotOccupancy of milestone.workOrder + .workOrderLotOccupancies) { + for (const occupants of lotOccupancy.lotOccupancyOccupants) { + calendarEvent.createAttendee({ + name: occupants.occupantName, + email: "no-reply@example.com" + }); + } + } + } + } + calendar.serve(response); +}; +export default handler; diff --git a/handlers/api-get/milestoneICS.ts b/handlers/api-get/milestoneICS.ts new file mode 100644 index 00000000..24f9a638 --- /dev/null +++ b/handlers/api-get/milestoneICS.ts @@ -0,0 +1,88 @@ +/* eslint-disable unicorn/filename-case */ + +import ical, { ICalEventData, ICalEventStatus } from "ical-generator"; + +import { getWorkOrderMilestones } from "../../helpers/lotOccupancyDB/getWorkOrderMilestones.js"; + +import type { RequestHandler } from "express"; +import { dateIntegerToString } from "@cityssm/expressjs-server-js/dateTimeFns.js"; + +const timeStringSplitRegex = /[ :-]/; + +export const handler: RequestHandler = (request, response) => { + const workOrderMilestones = getWorkOrderMilestones( + { + workOrderMilestoneDateFilter: "recent", + workOrderTypeIds: request.query.workOrderTypeIds as string, + workOrderMilestoneTypeIds: request.query + .workOrderMilestoneTypeIds as string + }, + { includeWorkOrders: true, orderBy: "date" } + ); + + const calendar = ical({ + name: "Work Order Milestone Calendar" + }); + + for (const milestone of workOrderMilestones) { + const milestoneTimePieces = ( + milestone.workOrderMilestoneDateString + + " " + + milestone.workOrderMilestoneTimeString + ).split(timeStringSplitRegex); + + const milestoneDate = new Date( + Number.parseInt(milestoneTimePieces[0], 10), + Number.parseInt(milestoneTimePieces[1], 10) - 1, + Number.parseInt(milestoneTimePieces[2], 10), + Number.parseInt(milestoneTimePieces[3], 10), + Number.parseInt(milestoneTimePieces[4], 10) + ); + + const eventData: ICalEventData = { + start: milestoneDate, + stamp: new Date(milestone.recordCreate_timeMillis), + lastModified: new Date(milestone.recordUpdate_timeMillis), + allDay: !milestone.workOrderMilestoneTime, + summary: milestone.workOrderMilestoneDescription + }; + + const calendarEvent = calendar.createEvent(eventData); + + if (milestone.workOrderMilestoneCompletionDate) { + calendarEvent.status(ICalEventStatus.CONFIRMED); + } + + if (milestone.workOrderMilestoneTypeId) { + calendarEvent.createCategory({ + name: milestone.workOrderMilestoneType + }); + } + + if (milestone.workOrder.workOrderLots.length > 0) { + const lotNames = []; + + for (const lot of milestone.workOrder.workOrderLots) { + lotNames.push(lot.mapName + ": " + lot.lotName); + } + + calendarEvent.location(lotNames.join(", ")); + } + + if (milestone.workOrder.workOrderLotOccupancies.length > 0) { + for (const lotOccupancy of milestone.workOrder + .workOrderLotOccupancies) { + for (const occupants of lotOccupancy.lotOccupancyOccupants) { + calendarEvent.createAttendee({ + name: occupants.occupantName, + email: "no-reply@example.com" + }); + } + } + } + } + + calendar.serve(response); +}; + +export default handler; diff --git a/handlers/permissions.d.ts b/handlers/permissions.d.ts index 3c411de8..1c1b6b1d 100644 --- a/handlers/permissions.d.ts +++ b/handlers/permissions.d.ts @@ -4,3 +4,4 @@ export declare const adminGetHandler: RequestHandler; export declare const adminPostHandler: RequestHandler; export declare const updateGetHandler: RequestHandler; export declare const updatePostHandler: RequestHandler; +export declare const apiGetHandler: RequestHandler; diff --git a/handlers/permissions.js b/handlers/permissions.js index 988c47b4..2b727b9c 100644 --- a/handlers/permissions.js +++ b/handlers/permissions.js @@ -31,3 +31,9 @@ export const updatePostHandler = (request, response, next) => { } return response.json(forbiddenJSON); }; +export const apiGetHandler = async (request, response, next) => { + if (await userFunctions.apiKeyIsValid(request)) { + return next(); + } + return response.redirect(urlPrefix + "/login"); +}; diff --git a/handlers/permissions.ts b/handlers/permissions.ts index 5cfb05d4..2383c6ac 100644 --- a/handlers/permissions.ts +++ b/handlers/permissions.ts @@ -44,3 +44,11 @@ export const updatePostHandler: RequestHandler = (request, response, next) => { return response.json(forbiddenJSON); }; + +export const apiGetHandler: RequestHandler = async (request, response, next) => { + if (await userFunctions.apiKeyIsValid(request)) { + return next(); + } + + return response.redirect(urlPrefix + "/login"); +}; \ No newline at end of file diff --git a/handlers/workOrders-get/outlook.d.ts b/handlers/workOrders-get/outlook.d.ts new file mode 100644 index 00000000..9621c611 --- /dev/null +++ b/handlers/workOrders-get/outlook.d.ts @@ -0,0 +1,3 @@ +import type { RequestHandler } from "express"; +export declare const handler: RequestHandler; +export default handler; diff --git a/handlers/workOrders-get/outlook.js b/handlers/workOrders-get/outlook.js new file mode 100644 index 00000000..8cef92a2 --- /dev/null +++ b/handlers/workOrders-get/outlook.js @@ -0,0 +1,11 @@ +import { getWorkOrderMilestoneTypes, getWorkOrderTypes } from "../../helpers/functions.cache.js"; +export const handler = (request, response) => { + const workOrderTypes = getWorkOrderTypes(); + const workOrderMilestoneTypes = getWorkOrderMilestoneTypes(); + response.render("workOrder-outlook", { + headTitle: "Work Order Outlook Integration", + workOrderTypes, + workOrderMilestoneTypes + }); +}; +export default handler; diff --git a/handlers/workOrders-get/outlook.ts b/handlers/workOrders-get/outlook.ts new file mode 100644 index 00000000..68ef16f2 --- /dev/null +++ b/handlers/workOrders-get/outlook.ts @@ -0,0 +1,16 @@ +import type { RequestHandler } from "express"; + +import { getWorkOrderMilestoneTypes, getWorkOrderTypes } from "../../helpers/functions.cache.js"; + +export const handler: RequestHandler = (request, response) => { + const workOrderTypes = getWorkOrderTypes(); + const workOrderMilestoneTypes = getWorkOrderMilestoneTypes(); + + response.render("workOrder-outlook", { + headTitle: "Work Order Outlook Integration", + workOrderTypes, + workOrderMilestoneTypes + }); +}; + +export default handler; diff --git a/helpers/functions.api.d.ts b/helpers/functions.api.d.ts index 5b4c4e9b..fef50de3 100644 --- a/helpers/functions.api.d.ts +++ b/helpers/functions.api.d.ts @@ -2,3 +2,4 @@ import * as recordTypes from "../types/recordTypes"; export declare const regenerateApiKey: (userName: string) => Promise; export declare const getApiKey: (userName: string) => Promise; export declare const getApiKeyFromSession: (session: recordTypes.PartialSession) => Promise; +export declare const getUserNameFromApiKey: (apiKey: string) => Promise; diff --git a/helpers/functions.api.js b/helpers/functions.api.js index b24d1385..8b6b07bb 100644 --- a/helpers/functions.api.js +++ b/helpers/functions.api.js @@ -41,3 +41,13 @@ export const getApiKey = async (userName) => { export const getApiKeyFromSession = async (session) => { return await getApiKey(session.user.userName); }; +export const getUserNameFromApiKey = async (apiKey) => { + if (!apiKeys) { + await loadApiKeys(); + } + for (const [userName, currentApiKey] of Object.entries(apiKeys)) { + if (apiKey === currentApiKey) { + return userName; + } + } +}; diff --git a/helpers/functions.api.ts b/helpers/functions.api.ts index c10d5346..a4af69e9 100644 --- a/helpers/functions.api.ts +++ b/helpers/functions.api.ts @@ -54,3 +54,15 @@ export const getApiKeyFromSession = async ( ) => { return await getApiKey(session.user.userName); }; + +export const getUserNameFromApiKey = async (apiKey: string) => { + if (!apiKeys) { + await loadApiKeys(); + } + + for (const [userName, currentApiKey] of Object.entries(apiKeys)) { + if (apiKey === currentApiKey) { + return userName; + } + } +}; diff --git a/helpers/functions.user.d.ts b/helpers/functions.user.d.ts index e9e19daa..74c0606a 100644 --- a/helpers/functions.user.d.ts +++ b/helpers/functions.user.d.ts @@ -1,3 +1,4 @@ import type { Request } from "express"; export declare const userIsAdmin: (request: Request) => boolean; export declare const userCanUpdate: (request: Request) => boolean; +export declare const apiKeyIsValid: (request: Request) => Promise; diff --git a/helpers/functions.user.js b/helpers/functions.user.js index 7cfb0976..c70cb5f6 100644 --- a/helpers/functions.user.js +++ b/helpers/functions.user.js @@ -1,3 +1,5 @@ +import { getUserNameFromApiKey } from "./functions.api.js"; +import * as configFunctions from "./functions.config.js"; export const userIsAdmin = (request) => { var _a; const user = (_a = request.session) === null || _a === void 0 ? void 0 : _a.user; @@ -14,3 +16,19 @@ export const userCanUpdate = (request) => { } return user.userProperties.canUpdate; }; +export const apiKeyIsValid = async (request) => { + const apiKey = request.params.apiKey; + if (!apiKey) { + return false; + } + const userName = await getUserNameFromApiKey(apiKey); + if (!userName) { + return false; + } + const canLogin = configFunctions + .getProperty("users.canLogin") + .some((currentUserName) => { + return userName === currentUserName.toLowerCase(); + }); + return canLogin; +}; diff --git a/helpers/functions.user.ts b/helpers/functions.user.ts index 27a812e4..a26c25c7 100644 --- a/helpers/functions.user.ts +++ b/helpers/functions.user.ts @@ -1,3 +1,6 @@ +import { getUserNameFromApiKey } from "./functions.api.js"; +import * as configFunctions from "./functions.config.js"; + import type { Request } from "express"; export const userIsAdmin = (request: Request): boolean => { @@ -19,3 +22,25 @@ export const userCanUpdate = (request: Request): boolean => { return user.userProperties.canUpdate; }; + +export const apiKeyIsValid = async (request: Request): Promise => { + const apiKey = request.params.apiKey; + + if (!apiKey) { + return false; + } + + const userName = await getUserNameFromApiKey(apiKey); + + if (!userName) { + return false; + } + + const canLogin = configFunctions + .getProperty("users.canLogin") + .some((currentUserName) => { + return userName === currentUserName.toLowerCase(); + }); + + return canLogin; +}; diff --git a/helpers/lotOccupancyDB/getWorkOrderMilestones.d.ts b/helpers/lotOccupancyDB/getWorkOrderMilestones.d.ts index 801ae46f..4581b0aa 100644 --- a/helpers/lotOccupancyDB/getWorkOrderMilestones.d.ts +++ b/helpers/lotOccupancyDB/getWorkOrderMilestones.d.ts @@ -4,6 +4,8 @@ interface WorkOrderMilestoneFilters { workOrderId?: number | string; workOrderMilestoneDateFilter?: "upcomingMissed" | "recent" | "date"; workOrderMilestoneDateString?: string; + workOrderTypeIds?: string; + workOrderMilestoneTypeIds?: string; } interface WorkOrderMilestoneOptions { includeWorkOrders?: boolean; diff --git a/helpers/lotOccupancyDB/getWorkOrderMilestones.js b/helpers/lotOccupancyDB/getWorkOrderMilestones.js index 1254d37e..c2223799 100644 --- a/helpers/lotOccupancyDB/getWorkOrderMilestones.js +++ b/helpers/lotOccupancyDB/getWorkOrderMilestones.js @@ -3,6 +3,7 @@ import { lotOccupancyDB as databasePath } from "../../data/databasePaths.js"; import { getWorkOrder } from "./getWorkOrder.js"; import { dateIntegerToString, dateStringToInteger, dateToInteger, timeIntegerToString } from "@cityssm/expressjs-server-js/dateTimeFns.js"; import * as configFunctions from "../functions.config.js"; +const commaSeparatedNumbersRegex = /^\d+(,\d+)*$/; export const getWorkOrderMilestones = (filters, options, connectedDatabase) => { const database = connectedDatabase || sqlite(databasePath, { @@ -41,6 +42,18 @@ export const getWorkOrderMilestones = (filters, options, connectedDatabase) => { sqlWhereClause += " and m.workOrderMilestoneDate = ?"; sqlParameters.push(dateStringToInteger(filters.workOrderMilestoneDateString)); } + if (filters.workOrderTypeIds && + commaSeparatedNumbersRegex.test(filters.workOrderTypeIds)) { + sqlWhereClause += + " and w.workOrderTypeId in (" + filters.workOrderTypeIds + ")"; + } + if (filters.workOrderMilestoneTypeIds && + commaSeparatedNumbersRegex.test(filters.workOrderMilestoneTypeIds)) { + sqlWhereClause += + " and m.workOrderMilestoneTypeId in (" + + filters.workOrderMilestoneTypeIds + + ")"; + } let orderByClause = ""; switch (options.orderBy) { case "completion": @@ -63,9 +76,11 @@ export const getWorkOrderMilestones = (filters, options, connectedDatabase) => { " m.workOrderMilestoneDescription," + " m.workOrderMilestoneCompletionDate, userFn_dateIntegerToString(m.workOrderMilestoneCompletionDate) as workOrderMilestoneCompletionDateString," + " m.workOrderMilestoneCompletionTime, userFn_timeIntegerToString(m.workOrderMilestoneCompletionTime) as workOrderMilestoneCompletionTimeString," + - " m.recordCreate_userName, m.recordUpdate_userName" + + " m.recordCreate_userName, m.recordCreate_timeMillis," + + " m.recordUpdate_userName, m.recordUpdate_timeMillis" + " from WorkOrderMilestones m" + " left join WorkOrderMilestoneTypes t on m.workOrderMilestoneTypeId = t.workOrderMilestoneTypeId" + + " left join WorkOrders w on m.workOrderId = w.workOrderId" + sqlWhereClause + orderByClause) .all(sqlParameters); diff --git a/helpers/lotOccupancyDB/getWorkOrderMilestones.ts b/helpers/lotOccupancyDB/getWorkOrderMilestones.ts index dc98cce8..6fd7d6b1 100644 --- a/helpers/lotOccupancyDB/getWorkOrderMilestones.ts +++ b/helpers/lotOccupancyDB/getWorkOrderMilestones.ts @@ -19,6 +19,8 @@ interface WorkOrderMilestoneFilters { workOrderId?: number | string; workOrderMilestoneDateFilter?: "upcomingMissed" | "recent" | "date"; workOrderMilestoneDateString?: string; + workOrderTypeIds?: string; + workOrderMilestoneTypeIds?: string; } interface WorkOrderMilestoneOptions { @@ -26,6 +28,8 @@ interface WorkOrderMilestoneOptions { orderBy: "completion" | "date"; } +const commaSeparatedNumbersRegex = /^\d+(,\d+)*$/; + export const getWorkOrderMilestones = ( filters: WorkOrderMilestoneFilters, options: WorkOrderMilestoneOptions, @@ -95,6 +99,24 @@ export const getWorkOrderMilestones = ( ); } + if ( + filters.workOrderTypeIds && + commaSeparatedNumbersRegex.test(filters.workOrderTypeIds) + ) { + sqlWhereClause += + " and w.workOrderTypeId in (" + filters.workOrderTypeIds + ")"; + } + + if ( + filters.workOrderMilestoneTypeIds && + commaSeparatedNumbersRegex.test(filters.workOrderMilestoneTypeIds) + ) { + sqlWhereClause += + " and m.workOrderMilestoneTypeId in (" + + filters.workOrderMilestoneTypeIds + + ")"; + } + // Order By let orderByClause = ""; @@ -125,9 +147,11 @@ export const getWorkOrderMilestones = ( " m.workOrderMilestoneDescription," + " m.workOrderMilestoneCompletionDate, userFn_dateIntegerToString(m.workOrderMilestoneCompletionDate) as workOrderMilestoneCompletionDateString," + " m.workOrderMilestoneCompletionTime, userFn_timeIntegerToString(m.workOrderMilestoneCompletionTime) as workOrderMilestoneCompletionTimeString," + - " m.recordCreate_userName, m.recordUpdate_userName" + + " m.recordCreate_userName, m.recordCreate_timeMillis," + + " m.recordUpdate_userName, m.recordUpdate_timeMillis" + " from WorkOrderMilestones m" + " left join WorkOrderMilestoneTypes t on m.workOrderMilestoneTypeId = t.workOrderMilestoneTypeId" + + " left join WorkOrders w on m.workOrderId = w.workOrderId" + sqlWhereClause + orderByClause ) diff --git a/package-lock.json b/package-lock.json index dc70ee2d..8f040854 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "express-rate-limit": "^6.6.0", "express-session": "^1.17.3", "http-errors": "^2.0.0", + "ical-generator": "^3.5.1", "leaflet": "^1.8.0", "papaparse": "^5.3.2", "randomcolor": "^0.6.2", @@ -1090,7 +1091,7 @@ "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", - "dev": true + "devOptional": true }, "node_modules/@types/ms": { "version": "0.7.31", @@ -1102,7 +1103,7 @@ "version": "18.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", - "dev": true + "devOptional": true }, "node_modules/@types/node-fetch": { "version": "2.6.2", @@ -3266,7 +3267,7 @@ "version": "1.11.5", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz", "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==", - "dev": true + "devOptional": true }, "node_modules/debug": { "version": "4.3.4", @@ -6315,6 +6316,57 @@ "node": ">=8.12.0" } }, + "node_modules/ical-generator": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-3.5.1.tgz", + "integrity": "sha512-OLCxRso9ulfkZeFY/aUzSZu9K/7IWnkJpjSG6coaNJXuToAyuBiCq4w1MG0cgSGzHQeY1WTVXez16fLwiZb5yg==", + "dependencies": { + "uuid-random": "^1.3.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@touch4it/ical-timezones": ">=1.6.0", + "@types/luxon": ">= 1.26.0", + "@types/mocha": ">= 8.2.1", + "@types/node": ">= 15.0.0", + "dayjs": ">= 1.10.0", + "luxon": ">= 1.26.0", + "moment": ">= 2.29.0", + "moment-timezone": ">= 0.5.33", + "rrule": ">= 2.6.8" + }, + "peerDependenciesMeta": { + "@touch4it/ical-timezones": { + "optional": true + }, + "@types/luxon": { + "optional": true + }, + "@types/mocha": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-timezone": { + "optional": true + }, + "rrule": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -11337,6 +11389,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuid-random": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", + "integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==" + }, "node_modules/v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", @@ -12792,7 +12849,7 @@ "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", - "dev": true + "devOptional": true }, "@types/ms": { "version": "0.7.31", @@ -12804,7 +12861,7 @@ "version": "18.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", - "dev": true + "devOptional": true }, "@types/node-fetch": { "version": "2.6.2", @@ -14447,7 +14504,7 @@ "version": "1.11.5", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz", "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==", - "dev": true + "devOptional": true }, "debug": { "version": "4.3.4", @@ -16834,6 +16891,14 @@ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true }, + "ical-generator": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-3.5.1.tgz", + "integrity": "sha512-OLCxRso9ulfkZeFY/aUzSZu9K/7IWnkJpjSG6coaNJXuToAyuBiCq4w1MG0cgSGzHQeY1WTVXez16fLwiZb5yg==", + "requires": { + "uuid-random": "^1.3.2" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -20709,6 +20774,11 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, + "uuid-random": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", + "integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==" + }, "v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", diff --git a/package.json b/package.json index 1dda6fa5..f5bfef4b 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "express-rate-limit": "^6.6.0", "express-session": "^1.17.3", "http-errors": "^2.0.0", + "ical-generator": "^3.5.1", "leaflet": "^1.8.0", "papaparse": "^5.3.2", "randomcolor": "^0.6.2", diff --git a/public-typescript/workOrderOutlook.d.ts b/public-typescript/workOrderOutlook.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/public-typescript/workOrderOutlook.js b/public-typescript/workOrderOutlook.js new file mode 100644 index 00000000..382fe153 --- /dev/null +++ b/public-typescript/workOrderOutlook.js @@ -0,0 +1,49 @@ +(() => { + const urlPrefix = document.querySelector("main").dataset.urlPrefix; + const apiKey = document.querySelector("main").dataset.apiKey; + const workOrderTypeIdsElement = document.querySelector("#icsFilters--workOrderTypeIds"); + const workOrderMilestoneTypeIdsElement = document.querySelector("#icsFilters--workOrderMilestoneTypeIds"); + const updateCalendarURL = () => { + let url = window.location.href.slice(0, Math.max(0, window.location.href.indexOf(window.location.pathname) + 1)) + + urlPrefix + + "api/" + + apiKey + + "/" + + "milestoneICS/" + + "?"; + if (!workOrderTypeIdsElement.disabled && + workOrderTypeIdsElement.selectedOptions.length > 0) { + url += "workOrderTypeIds="; + for (const optionElement of workOrderTypeIdsElement.selectedOptions) { + url += optionElement.value + ","; + } + url = url.slice(0, -1) + "&"; + } + if (!workOrderMilestoneTypeIdsElement.disabled && + workOrderMilestoneTypeIdsElement.selectedOptions.length > 0) { + url += "workOrderMilestoneTypeIds="; + for (const optionElement of workOrderMilestoneTypeIdsElement.selectedOptions) { + url += optionElement.value + ","; + } + url = url.slice(0, -1) + "&"; + } + document.querySelector("#icsFilters--calendarURL").value = url.slice(0, -1); + }; + document + .querySelector("#icsFilters--workOrderTypeIds-all") + .addEventListener("change", (changeEvent) => { + workOrderTypeIdsElement.disabled = changeEvent.currentTarget.checked; + }); + document + .querySelector("#icsFilters--workOrderMilestoneTypeIds-all") + .addEventListener("change", (changeEvent) => { + workOrderMilestoneTypeIdsElement.disabled = changeEvent.currentTarget.checked; + }); + const inputSelectElements = document + .querySelector("#panel--icsFilters") + .querySelectorAll("input, select"); + for (const element of inputSelectElements) { + element.addEventListener("change", updateCalendarURL); + } + updateCalendarURL(); +})(); diff --git a/public-typescript/workOrderOutlook.ts b/public-typescript/workOrderOutlook.ts new file mode 100644 index 00000000..5b62017f --- /dev/null +++ b/public-typescript/workOrderOutlook.ts @@ -0,0 +1,89 @@ +(() => { + const urlPrefix = document.querySelector("main").dataset.urlPrefix; + const apiKey = document.querySelector("main").dataset.apiKey; + + const workOrderTypeIdsElement = document.querySelector( + "#icsFilters--workOrderTypeIds" + ) as HTMLSelectElement; + + const workOrderMilestoneTypeIdsElement = document.querySelector( + "#icsFilters--workOrderMilestoneTypeIds" + ) as HTMLSelectElement; + + const updateCalendarURL = () => { + let url = + window.location.href.slice( + 0, + Math.max( + 0, + window.location.href.indexOf(window.location.pathname) + 1 + ) + ) + + urlPrefix + + "api/" + + apiKey + + "/" + + "milestoneICS/" + + "?"; + + if ( + !workOrderTypeIdsElement.disabled && + workOrderTypeIdsElement.selectedOptions.length > 0 + ) { + url += "workOrderTypeIds="; + + for (const optionElement of workOrderTypeIdsElement.selectedOptions) { + url += optionElement.value + ","; + } + + url = url.slice(0, -1) + "&"; + } + + if ( + !workOrderMilestoneTypeIdsElement.disabled && + workOrderMilestoneTypeIdsElement.selectedOptions.length > 0 + ) { + url += "workOrderMilestoneTypeIds="; + + for (const optionElement of workOrderMilestoneTypeIdsElement.selectedOptions) { + url += optionElement.value + ","; + } + + url = url.slice(0, -1) + "&"; + } + + ( + document.querySelector( + "#icsFilters--calendarURL" + ) as HTMLTextAreaElement + ).value = url.slice(0, -1); + }; + + document + .querySelector("#icsFilters--workOrderTypeIds-all") + .addEventListener("change", (changeEvent) => { + workOrderTypeIdsElement.disabled = ( + changeEvent.currentTarget as HTMLInputElement + ).checked; + }); + + document + .querySelector("#icsFilters--workOrderMilestoneTypeIds-all") + .addEventListener("change", (changeEvent) => { + workOrderMilestoneTypeIdsElement.disabled = ( + changeEvent.currentTarget as HTMLInputElement + ).checked; + }); + + const inputSelectElements = document + .querySelector("#panel--icsFilters") + .querySelectorAll("input, select") as NodeListOf< + HTMLInputElement | HTMLSelectElement + >; + + for (const element of inputSelectElements) { + element.addEventListener("change", updateCalendarURL); + } + + updateCalendarURL(); +})(); diff --git a/public/javascripts/workOrderOutlook.min.js b/public/javascripts/workOrderOutlook.min.js new file mode 100644 index 00000000..94c5f570 --- /dev/null +++ b/public/javascripts/workOrderOutlook.min.js @@ -0,0 +1 @@ +(()=>{const e=document.querySelector("main").dataset.urlPrefix,t=document.querySelector("main").dataset.apiKey,r=document.querySelector("#icsFilters--workOrderTypeIds"),o=document.querySelector("#icsFilters--workOrderMilestoneTypeIds"),c=()=>{let c=window.location.href.slice(0,Math.max(0,window.location.href.indexOf(window.location.pathname)+1))+e+"api/"+t+"/milestoneICS/?";if(!r.disabled&&r.selectedOptions.length>0){c+="workOrderTypeIds=";for(const e of r.selectedOptions)c+=e.value+",";c=c.slice(0,-1)+"&"}if(!o.disabled&&o.selectedOptions.length>0){c+="workOrderMilestoneTypeIds=";for(const e of o.selectedOptions)c+=e.value+",";c=c.slice(0,-1)+"&"}document.querySelector("#icsFilters--calendarURL").value=c.slice(0,-1)};document.querySelector("#icsFilters--workOrderTypeIds-all").addEventListener("change",e=>{r.disabled=e.currentTarget.checked}),document.querySelector("#icsFilters--workOrderMilestoneTypeIds-all").addEventListener("change",e=>{o.disabled=e.currentTarget.checked});const l=document.querySelector("#panel--icsFilters").querySelectorAll("input, select");for(const e of l)e.addEventListener("change",c);c()})(); \ No newline at end of file diff --git a/routes/api.d.ts b/routes/api.d.ts new file mode 100644 index 00000000..433ab333 --- /dev/null +++ b/routes/api.d.ts @@ -0,0 +1,2 @@ +export declare const router: import("express-serve-static-core").Router; +export default router; diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 00000000..1d88ade9 --- /dev/null +++ b/routes/api.js @@ -0,0 +1,5 @@ +import { Router } from "express"; +import handler_milestoneICS from "../handlers/api-get/milestoneICS.js"; +export const router = Router(); +router.get("/milestoneICS", handler_milestoneICS); +export default router; diff --git a/routes/api.ts b/routes/api.ts new file mode 100644 index 00000000..281b79fb --- /dev/null +++ b/routes/api.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; + +import handler_milestoneICS from "../handlers/api-get/milestoneICS.js"; + +export const router = Router(); + +router.get("/milestoneICS", handler_milestoneICS); + +export default router; diff --git a/routes/workOrders.js b/routes/workOrders.js index 05dd8928..e10aeba3 100644 --- a/routes/workOrders.js +++ b/routes/workOrders.js @@ -4,6 +4,7 @@ import handler_search from "../handlers/workOrders-get/search.js"; import handler_doSearchWorkOrders from "../handlers/workOrders-post/doSearchWorkOrders.js"; import handler_milestoneCalendar from "../handlers/workOrders-get/milestoneCalendar.js"; import handler_doGetWorkOrderMilestones from "../handlers/workOrders-post/doGetWorkOrderMilestones.js"; +import handler_outlook from "../handlers/workOrders-get/outlook.js"; import handler_view from "../handlers/workOrders-get/view.js"; import handler_doReopenWorkOrder from "../handlers/workOrders-post/doReopenWorkOrder.js"; import handler_new from "../handlers/workOrders-get/new.js"; @@ -26,6 +27,7 @@ router.get("/", handler_search); router.post("/doSearchWorkOrders", handler_doSearchWorkOrders); router.get("/milestoneCalendar", handler_milestoneCalendar); router.post("/doGetWorkOrderMilestones", handler_doGetWorkOrderMilestones); +router.get("/outlook", handler_outlook); router.get("/new", permissionHandlers.adminGetHandler, handler_new); router.post("/doCreateWorkOrder", permissionHandlers.updatePostHandler, handler_doCreateWorkOrder); router.get("/:workOrderId", handler_view); diff --git a/routes/workOrders.ts b/routes/workOrders.ts index 994f05e4..c6e941e8 100644 --- a/routes/workOrders.ts +++ b/routes/workOrders.ts @@ -8,6 +8,8 @@ import handler_doSearchWorkOrders from "../handlers/workOrders-post/doSearchWork import handler_milestoneCalendar from "../handlers/workOrders-get/milestoneCalendar.js"; import handler_doGetWorkOrderMilestones from "../handlers/workOrders-post/doGetWorkOrderMilestones.js"; +import handler_outlook from "../handlers/workOrders-get/outlook.js"; + import handler_view from "../handlers/workOrders-get/view.js"; import handler_doReopenWorkOrder from "../handlers/workOrders-post/doReopenWorkOrder.js"; @@ -45,6 +47,10 @@ router.get("/milestoneCalendar", handler_milestoneCalendar); router.post("/doGetWorkOrderMilestones", handler_doGetWorkOrderMilestones); +// Outlook Integration + +router.get("/outlook", handler_outlook); + // New router.get("/new", permissionHandlers.adminGetHandler, handler_new); diff --git a/views/_header.ejs b/views/_header.ejs index de54145b..9941163a 100644 --- a/views/_header.ejs +++ b/views/_header.ejs @@ -106,6 +106,8 @@
" - data-is-admin="<%= user.userProperties.isAdmin ? "true" : "false" %>"> \ No newline at end of file + data-is-admin="<%= user.userProperties.isAdmin ? "true" : "false" %>" + data-api-key="<%= user.userProperties.apiKey %>"> \ No newline at end of file diff --git a/views/_menu-workOrders.ejs b/views/_menu-workOrders.ejs index d034dc17..c51e311b 100644 --- a/views/_menu-workOrders.ejs +++ b/views/_menu-workOrders.ejs @@ -9,11 +9,22 @@ Work Order Search + + + \ No newline at end of file diff --git a/views/workOrder-outlook.ejs b/views/workOrder-outlook.ejs new file mode 100644 index 00000000..e1f059d9 --- /dev/null +++ b/views/workOrder-outlook.ejs @@ -0,0 +1,86 @@ +<%- include('_header'); -%> + +
+
+ <%- include('_menu-workOrders'); -%> +
+
+ + +

+ Outlook Integration +

+ +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +<%- include('_footerA'); -%> + + + +<%- include('_footerB'); -%> \ No newline at end of file