Skip to content

Commit

Permalink
Merge pull request #55 from icefoganalytics/main
Browse files Browse the repository at this point in the history
Updates
  • Loading branch information
datajohnson authored Feb 19, 2025
2 parents fcb3e1d + e781578 commit 5820361
Show file tree
Hide file tree
Showing 22 changed files with 854 additions and 161 deletions.
24 changes: 19 additions & 5 deletions api/src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { NextFunction, Request, Response } from "express";
import { validationResult } from "express-validator";
import { isNil } from "lodash";
import { UserRole } from "../data/models";

export async function ReturnValidationErrors(req: Request, res: Response, next: NextFunction) {
const errors = validationResult(req);
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

next();
next();
}

export async function RequireAdmin(req: Request, res: Response, next: NextFunction) {
if (isNil(req.user.roles || req.user.roles.length == 0)) {
return res.status(403).json({ error: "Unauthorized" });
}

if (!req.user.roles.filter((role: UserRole) => role.name === "System Admin").length) {
return res.status(403).json({ error: "Unauthorized" });
}

next();
}
28 changes: 27 additions & 1 deletion api/src/routes/location-router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import express, { Request, Response } from "express";

import { db as knex } from "../data";
import { RequireAdmin } from "../middleware";
import { checkJwt, loadUser } from "../middleware/authz.middleware";

export const locationRouter = express.Router();

locationRouter.get("/", async (_req: Request, res: Response) => {
const list = await knex("locations");
const list = await knex("locations").orderBy("name", "asc");
res.json({ data: list });
});

locationRouter.post("/", checkJwt, loadUser, RequireAdmin, async (req: Request, res: Response) => {
const { code, name, description } = req.body;

try {
const list = await knex("locations").insert({ code, name, description });

res.json({ data: list });
} catch (err) {
return res.status(400).json({ error: err });
}
});

locationRouter.put("/:code", async (req: Request, res: Response) => {
const { code } = req.params;
const { name, description } = req.body;

try {
const list = await knex("locations").where({ code }).update({ name, description });
res.json({ data: list });
} catch (err) {
return res.status(400).json({ error: err });
}
});
88 changes: 65 additions & 23 deletions api/src/routes/report-router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import express, { Request, Response } from "express";
import { isArray, isEmpty } from "lodash";
import { create, isArray, isEmpty } from "lodash";

import { db as knex } from "../data";
import { DepartmentService, DirectoryService, EmailService, IncidentService } from "../services";
Expand Down Expand Up @@ -60,9 +60,11 @@ reportRouter.get("/:id", async (req: Request, res: Response) => {

reportRouter.put("/:id", async (req: Request, res: Response) => {
const { id } = req.params;
const { description, investigation_notes, additional_description } = req.body;
const { description, investigation_notes, additional_description, urgency_code } = req.body;

await knex("incidents").where({ id }).update({ description, investigation_notes, additional_description });
await knex("incidents")
.where({ id })
.update({ description, investigation_notes, additional_description, urgency_code });

return res.json({ data: {}, messages: [{ variant: "success", text: "Incident Saved" }] });
});
Expand Down Expand Up @@ -351,6 +353,43 @@ reportRouter.put("/:id/step/:step_id/:operation", async (req: Request, res: Resp
return res.json({ data: {} });
});

reportRouter.post("/:id/send-notification", async (req: Request, res: Response) => {
const { id } = req.params;
const { recipients } = req.body;

const incident = await knex("incidents").where({ id }).first();
if (!incident) return res.status(404).send();

const recipientList = recipients.split(/[\s,;]+/).filter(Boolean);

for (const recipient of recipientList) {
const directorySubmitter = await directoryService.searchByEmail(recipient);
const employeeName = directorySubmitter && directorySubmitter[0] ? directorySubmitter[0].display_name : recipient;

await emailService.sendIncidentInviteNotification({ fullName: employeeName, email: recipient }, incident);
}

return res.json({ data: {}, messages: [{ variant: "success", text: "Email Sent" }] });
});

reportRouter.post("/:id/send-employee-notification", async (req: Request, res: Response) => {
const { id } = req.params;

const incident = await knex("incidents").where({ id }).first();
if (!incident) return res.status(404).send();

const directorySubmitter = await directoryService.searchByEmail(incident.reporting_person_email);
const employeeName =
directorySubmitter && directorySubmitter[0] ? directorySubmitter[0].display_name : incident.reporting_person_email;

await emailService.sendIncidentCompleteEmployeeNotification(
{ fullName: employeeName, email: incident.reporting_person_email },
incident
);

return res.json({ data: {}, messages: [{ variant: "success", text: "Email Sent" }] });
});

reportRouter.post("/:id/action", async (req: Request, res: Response) => {
const { id } = req.params;
const {
Expand All @@ -362,6 +401,7 @@ reportRouter.post("/:id/action", async (req: Request, res: Response) => {
actor_role_type_id,
due_date,
create_hazard,
create_action,
hazard_type_id,
urgency_code,
} = req.body;
Expand Down Expand Up @@ -405,28 +445,30 @@ reportRouter.post("/:id/action", async (req: Request, res: Response) => {
await knex("incident_hazards").insert(link);
}

const action = {
incident_id: parseInt(id),
hazard_id,
created_at: InsertableDate(DateTime.utc().toISO()),
description: `${incident.incident_type_description.replace(/\(.*\)/g, "")} ${description}`,
notes,
action_type_code: ActionTypes.USER_GENERATED.code,
sensitivity_code: SensitivityLevels.NOT_SENSITIVE.code,
status_code: ActionStatuses.OPEN.code,
actor_user_email,
actor_user_id,
actor_role_type_id,
due_date: InsertableDate(due_date),
} as Action;
if (create_action) {
const action = {
incident_id: parseInt(id),
hazard_id,
created_at: InsertableDate(DateTime.utc().toISO()),
description: `${incident.incident_type_description.replace(/\(.*\)/g, "")} ${description}`,
notes,
action_type_code: ActionTypes.USER_GENERATED.code,
sensitivity_code: SensitivityLevels.NOT_SENSITIVE.code,
status_code: ActionStatuses.OPEN.code,
actor_user_email,
actor_user_id,
actor_role_type_id,
due_date: InsertableDate(due_date),
} as Action;

await knex("actions").insert(action);
await knex("actions").insert(action);

if (actor_user_email) {
await emailService.sendTaskAssignmentNotification(
{ fullName: actor_display_name, email: actor_user_email },
action
);
if (actor_user_email) {
await emailService.sendTaskAssignmentNotification(
{ fullName: actor_display_name, email: actor_user_email },
action
);
}
}

return res.json({ data: {}, messages: [{ variant: "success", text: "Task Saved" }] });
Expand Down
31 changes: 31 additions & 0 deletions api/src/services/email-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const INCIDENT_REPORTER_TEMPLATE = "../templates/email/incident-notification-rep
const INCIDENT_SUPERVISOR_TEMPLATE = "../templates/email/incident-notification-supervisor.html";
const TASK_ASSIGNED_TEMPLATE = "../templates/email/task-assigned-notification.html";

const INCIDENT_EMPLOYEE_COMPLETE_TEMPLATE = "../templates/email/incident-complete-employee.html";
const INCIDENT_INVITE_TEMPLATE = "../templates/email/incident-invite.html";

export class EmailService {
transport: Transporter;

Expand Down Expand Up @@ -65,6 +68,34 @@ export class EmailService {
await this.sendEmail(recipient.fullName, recipient.email, "We Received Your Report", content);
}

async sendIncidentCompleteEmployeeNotification(
recipient: { fullName: string; email: string },
incident: Incident
): Promise<any> {
let templatePath = path.join(__dirname, INCIDENT_EMPLOYEE_COMPLETE_TEMPLATE);
let content = fs.readFileSync(templatePath).toString();

content = content.replace(/``INCIDENT_URL``/g, `${FRONTEND_OVERRIDE}/reports/${incident.id}`);

console.log("-- EMAIL EMPLOYEE INCIDENT COMPLETE", recipient.email);

await this.sendEmail(recipient.fullName, recipient.email, "Your Report Is Complete", content);
}

async sendIncidentInviteNotification(
recipient: { fullName: string; email: string },
incident: Incident
): Promise<any> {
let templatePath = path.join(__dirname, INCIDENT_INVITE_TEMPLATE);
let content = fs.readFileSync(templatePath).toString();

content = content.replace(/``INCIDENT_URL``/g, `${FRONTEND_OVERRIDE}/reports/${incident.id}`);

console.log("-- EMAIL INVITE INCIDENT NOTIFICATION", recipient.email);

await this.sendEmail(recipient.fullName, recipient.email, "You Have Been Invited To A Report", content);
}

async sendIncidentSupervisorNotification(
recipient: { fullName: string; email: string },
employeeName: string,
Expand Down
8 changes: 8 additions & 0 deletions api/src/templates/email/incident-complete-employee.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<p style="margin: 0; margin-bottom: 12px; line-height: 24px; font-size: 16px">
This email is to notify you that the investigation and resolution of an incident you reported has been completed. The
incident has been marked as closed.
</p>

<p style="margin: 0; margin-bottom: 12px; line-height: 24px; font-size: 16px">
To view the details of the incident, please visit <a href="``INCIDENT_URL``">``INCIDENT_URL``</a>.
</p>
8 changes: 8 additions & 0 deletions api/src/templates/email/incident-invite.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<p style="margin: 0; margin-bottom: 12px; line-height: 24px; font-size: 16px">
This email is to notify you that you have been invited to view or contribute to a reported incident in the YG Safety
Portal.
</p>

<p style="margin: 0; margin-bottom: 12px; line-height: 24px; font-size: 16px">
To view the details of the incident, please visit <a href="``INCIDENT_URL``">``INCIDENT_URL``</a>.
</p>
113 changes: 67 additions & 46 deletions web/src/components/action/ActionEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,51 +35,72 @@
<v-card-text>
<h3>{{ currentStep.title }}</h3>

<v-checkbox v-model="categories" value="Chemical" hide-details density="compact">
<template #label
>Chemical
<v-tooltip location="right" activator="parent" width="600">
Examples: Chemical Asbestos, chemical storage, chemicals, dust/smoke/fumes, lead paint, mists and
vapours, radon gas
</v-tooltip>
</template>
</v-checkbox>
<v-checkbox v-model="categories" value="Biological" hide-details density="compact">
<template #label
>Biological
<v-tooltip location="right" activator="parent" width="600">
Examples: Blood and bodily fluids, human waste, insect/animal bite, medical waste, mold,
viruses/bacteria
</v-tooltip>
</template>
</v-checkbox>

<v-checkbox v-model="categories" value="Ergonomic" hide-details density="compact">
<template #label
>Ergonomic
<v-tooltip location="right" activator="parent" width="600">
Examples: Improper lifting, improper workstations, repetitive activity, strenuous activity
</v-tooltip>
</template>
</v-checkbox>
<v-checkbox v-model="categories" value="Physical Conditions" hide-details density="compact">
<template #label
>Physical Conditions
<v-tooltip location="right" activator="parent" width="600">
Examples: Electrical, temperature, humidity, fire/explosion potential, housekeeping, lighting,
pressure systems, road conditions, slippery or uneven surface, vibration, wildlife, working alone
</v-tooltip>
</template>
</v-checkbox>
<v-checkbox v-model="categories" value="Safety" hide-details density="compact">
<template #label
>Safety
<v-tooltip location="right" activator="parent" width="600">
Examples: Blocked exit routes, confined space, falling from heights, falling items, faulty
equipment, machinery in motion, overhead hazard, pinch/nip points, sharp objects
</v-tooltip>
</template>
</v-checkbox>
<div class="d-flex">
<v-checkbox v-model="categories" value="Chemical" hide-details density="compact" label="Chemical" />
<v-tooltip location="top" width="600" open-delay="250">
<template #activator="{ props }">
<v-icon color="primary" class="ml-2 pt-4 cursor-pointer" v-bind="props">mdi-information</v-icon>
</template>
Examples: Chemical Asbestos, chemical storage, chemicals, dust/smoke/fumes, lead paint, mists and
vapours, radon gas
</v-tooltip>
</div>

<div class="d-flex">
<v-checkbox
v-model="categories"
value="Biological"
hide-details
density="compact"
label="Biological" />
<v-tooltip location="top" width="600" open-delay="250">
<template #activator="{ props }">
<v-icon color="primary" class="ml-2 pt-4 cursor-pointer" v-bind="props">mdi-information</v-icon>
</template>
Examples: Blood and bodily fluids, human waste, insect/animal bite, medical waste, mold,
viruses/bacteria
</v-tooltip>
</div>

<div class="d-flex">
<v-checkbox v-model="categories" value="Ergonomic" hide-details density="compact" label="Ergonomic" />
<v-tooltip location="top" width="600" open-delay="250">
<template #activator="{ props }">
<v-icon color="primary" class="ml-2 pt-4 cursor-pointer" v-bind="props">mdi-information</v-icon>
</template>
Examples: Improper lifting, improper workstations, repetitive activity, strenuous activity
</v-tooltip>
</div>

<div class="d-flex">
<v-checkbox
v-model="categories"
value="Physical Conditions"
hide-details
density="compact"
label="Physical Conditions" />

<v-tooltip location="top" width="600" open-delay="250">
<template #activator="{ props }">
<v-icon color="primary" class="ml-2 pt-4 cursor-pointer" v-bind="props">mdi-information</v-icon>
</template>
Examples: Electrical, temperature, humidity, fire/explosion potential, housekeeping, lighting,
pressure systems, road conditions, slippery or uneven surface, vibration, wildlife, working alone
</v-tooltip>
</div>

<div class="d-flex">
<v-checkbox v-model="categories" value="Safety" hide-details density="compact" label="Safety" />
<v-tooltip location="top" width="600" open-delay="250">
<template #activator="{ props }">
<v-icon color="primary" class="ml-2 pt-4 cursor-pointer" style="opacity: 1" v-bind="props"
>mdi-information</v-icon
>
</template>
Examples: Blocked exit routes, confined space, falling from heights, falling items, faulty
equipment, machinery in motion, overhead hazard, pinch/nip points, sharp objects
</v-tooltip>
</div>
</v-card-text>
</v-window-item>

Expand Down Expand Up @@ -286,7 +307,7 @@
import { ref, computed, defineProps, watch } from "vue";
import { storeToRefs } from "pinia";
import { DateTime } from "luxon";
import { isNil, isNumber } from "lodash";
import { isNil, isNumber } from "lodash";
import { useUserStore } from "@/store/UserStore";
import { useReportStore } from "@/store/ReportStore";
Expand Down
Loading

0 comments on commit 5820361

Please sign in to comment.