outlook integration
parent
326cac8717
commit
f0f9c6dfe5
3
app.js
3
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);
|
||||
|
|
|
|||
4
app.ts
4
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);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import type { RequestHandler } from "express";
|
||||
export declare const handler: RequestHandler;
|
||||
export default handler;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import type { RequestHandler } from "express";
|
||||
export declare const handler: RequestHandler;
|
||||
export default handler;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -2,3 +2,4 @@ import * as recordTypes from "../types/recordTypes";
|
|||
export declare const regenerateApiKey: (userName: string) => Promise<void>;
|
||||
export declare const getApiKey: (userName: string) => Promise<string>;
|
||||
export declare const getApiKeyFromSession: (session: recordTypes.PartialSession) => Promise<string>;
|
||||
export declare const getUserNameFromApiKey: (apiKey: string) => Promise<string>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<boolean>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<boolean> => {
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ interface WorkOrderMilestoneFilters {
|
|||
workOrderId?: number | string;
|
||||
workOrderMilestoneDateFilter?: "upcomingMissed" | "recent" | "date";
|
||||
workOrderMilestoneDateString?: string;
|
||||
workOrderTypeIds?: string;
|
||||
workOrderMilestoneTypeIds?: string;
|
||||
}
|
||||
interface WorkOrderMilestoneOptions {
|
||||
includeWorkOrders?: boolean;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})();
|
||||
|
|
@ -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();
|
||||
})();
|
||||
|
|
@ -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()})();
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@
|
|||
</nav>
|
||||
|
||||
<main class="container pt-2 px-3 mr-auto has-min-page-height"
|
||||
data-session-keep-alive-millis="<%= configFunctions.keepAliveMillis %>" data-url-prefix="<%= urlPrefix %>"
|
||||
data-session-keep-alive-millis="<%= configFunctions.keepAliveMillis %>"
|
||||
data-url-prefix="<%= urlPrefix %>"
|
||||
data-can-update="<%= user.userProperties.canUpdate ? "true" : "false" %>"
|
||||
data-is-admin="<%= user.userProperties.isAdmin ? "true" : "false" %>">
|
||||
data-is-admin="<%= user.userProperties.isAdmin ? "true" : "false" %>"
|
||||
data-api-key="<%= user.userProperties.apiKey %>">
|
||||
|
|
@ -9,11 +9,22 @@
|
|||
<span>Work Order Search</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="menu-label">
|
||||
Milestones
|
||||
</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a class="<%= (headTitle.endsWith("Milestone Calendar") ? "is-active" : "") %>" href="<%= urlPrefix %>/workOrders/milestoneCalendar">
|
||||
<span class="icon is-small"><i class="fas fa-fw fa-calendar" aria-hidden="true"></i></span>
|
||||
<span>Milestone Calendar</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="<%= (headTitle.endsWith("Outlook Integration") ? "is-active" : "") %>" href="<%= urlPrefix %>/workOrders/outlook">
|
||||
<span class="icon is-small"><i class="fas fa-fw fa-envelope-open-text" aria-hidden="true"></i></span>
|
||||
<span>Outlook Integration</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<%- include('_header'); -%>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3 is-hidden-mobile">
|
||||
<%- include('_menu-workOrders'); -%>
|
||||
</div>
|
||||
<div class="column">
|
||||
<nav class="breadcrumb">
|
||||
<ul>
|
||||
<li><a href="<%= urlPrefix %>/dashboard">Home</a></li>
|
||||
<li>
|
||||
<a href="<%= urlPrefix %>/workOrders">
|
||||
<span class="icon is-small"><i class="fas fa-hard-hat" aria-hidden="true"></i></span>
|
||||
<span>Work Orders</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
Outlook Integration
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<h1 class="title is-1">
|
||||
Outlook Integration
|
||||
</h1>
|
||||
|
||||
<div class="panel" id="panel--icsFilters">
|
||||
<div class="panel-block is-block">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<label class="label" for="icsFilters--workOrderTypeIds">Work Order Types</label>
|
||||
<label class="checkbox is-block">
|
||||
<input id="icsFilters--workOrderTypeIds-all" type="checkbox" checked />
|
||||
All Work Order Types
|
||||
</label>
|
||||
<div class="control mt-2">
|
||||
<div class="select is-multiple is-fullwidth">
|
||||
<select id="icsFilters--workOrderTypeIds" multiple size="<%= Math.min(Math.max(workOrderTypes.length, workOrderMilestoneTypes.length), 6) %>" disabled>
|
||||
<% for (const workOrderType of workOrderTypes) { %>
|
||||
<option value="<%= workOrderType.workOrderTypeId %>" selected>
|
||||
<%= workOrderType.workOrderType %>
|
||||
</option>
|
||||
<% } %>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<label class="label" for="icsFilters--workOrderMilestoneTypeIds">Milestone Types</label>
|
||||
<label class="checkbox is-block">
|
||||
<input id="icsFilters--workOrderMilestoneTypeIds-all" type="checkbox" checked />
|
||||
All Work Order Milestone Types
|
||||
</label>
|
||||
<div class="control mt-2">
|
||||
<div class="select is-multiple is-fullwidth">
|
||||
<select id="icsFilters--workOrderMilestoneTypeIds" multiple size="<%= Math.min(Math.max(workOrderTypes.length, workOrderMilestoneTypes.length), 6) %>" disabled>
|
||||
<% for (const workOrderMilestoneType of workOrderMilestoneTypes) { %>
|
||||
<option value="<%= workOrderMilestoneType.workOrderMilestoneTypeId %>" selected>
|
||||
<%= workOrderMilestoneType.workOrderMilestoneType %>
|
||||
</option>
|
||||
<% } %>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-block is-block">
|
||||
<div class="field">
|
||||
<label class="label" for="icsFilters--calendarURL">ICS Calendar Link</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" id="icsFilters--calendarURL" name="calendarURL" style="cursor:text" disabled readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('_footerA'); -%>
|
||||
|
||||
<script src="<%= urlPrefix %>/javascripts/workOrderOutlook.min.js"></script>
|
||||
|
||||
<%- include('_footerB'); -%>
|
||||
Loading…
Reference in New Issue