Skip to content

Commit

Permalink
feat: CE-1082-Add-attachment-summary-to-PDF-exports (#828)
Browse files Browse the repository at this point in the history
Co-authored-by: afwilcox <[email protected]>
  • Loading branch information
dk-bcps and afwilcox authored Dec 17, 2024
1 parent 71ea769 commit cf77894
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 20 deletions.
34 changes: 34 additions & 0 deletions backend/src/common/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,37 @@ export const formatPhonenumber = (input: string): string => {
//-- return the phone number as its stored
return input;
};

export const getFileType = (input: string): string => {
const extension = getFileExtension(input);
return mapExtensiontoFileType(extension);
};

export const getFileExtension = (input: string): string => {
return input
.substring(input.lastIndexOf(".") + 1)
.toLowerCase()
.trim();
};

export const mapExtensiontoFileType = (input: string): string => {
if (["bmp", "gif", "heif", "heic", "jpg", "jpeg", "png", "psd", "svg", "tif", "tiff"].includes(input)) {
return "Image";
}
if (["doc", "docx", "md", "odt", "pdf", "ppt", "rtf", "txt", "xls", "xlsx"].includes(input)) {
return "Document";
}
if (["flac", "mp3", "aac", "ogg", "wma", "wav", "wave"].includes(input)) {
return "Audio";
}
if (["avi", "flv", "mov", "mp4"].includes(input)) {
return "Video";
}
if (["7z", "jar", "rar", "zip"].includes(input)) {
return "Archive";
}
if (["eml", "msg", "ost", "pst"].includes(input)) {
return "Email";
}
return "Unknown";
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { COMPLAINT_TYPE } from "./complaint-type";

export interface ExportComplaintParameters {
id: string;
type: COMPLAINT_TYPE;
tz: string;
attachments: any;
}
13 changes: 13 additions & 0 deletions backend/src/types/models/general/attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface Attachment {
type: AttachmentType;
date: Date;
name: string;
user: string;
sequenceId: number;
fileType: string;
}

export enum AttachmentType {
COMPLAINT_ATTACHMENT = "COMPLAINT_ATTACHMENT",
OUTCOME_ATTACHMENT = "OUTCOME_ATTACHMENT",
}
34 changes: 32 additions & 2 deletions backend/src/v1/complaint/complaint.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ import { CompMthdRecvCdAgcyCdXrefService } from "../comp_mthd_recv_cd_agcy_cd_xr
import { OfficerService } from "../officer/officer.service";
import { SpeciesCode } from "../species_code/entities/species_code.entity";
import { LinkedComplaintXrefService } from "../linked_complaint_xref/linked_complaint_xref.service";

import { Attachment, AttachmentType } from "../../types/models/general/attachment";
import { getFileType } from "../../common/methods";
const WorldBounds: Array<number> = [-180, -90, 180, 90];
type complaintAlias = HwcrComplaint | AllegationComplaint | GirComplaint;
@Injectable({ scope: Scope.REQUEST })
Expand Down Expand Up @@ -1717,7 +1718,13 @@ export class ComplaintService {
return results;
};

getReportData = async (id: string, complaintType: COMPLAINT_TYPE, tz: string, token: string) => {
getReportData = async (
id: string,
complaintType: COMPLAINT_TYPE,
tz: string,
token: string,
attachments: Attachment[],
) => {
let data;
mapWildlifeReport(this.mapper, tz);
mapAllegationReport(this.mapper, tz);
Expand Down Expand Up @@ -2184,6 +2191,29 @@ export class ComplaintService {
if (data.incidentDateTime) {
data.incidentDateTime = _applyTimezone(data.incidentDateTime, tz, "datetime");
}
// Using short names like "cAtts" and "oAtts" to fit them in CDOGS template table cells
data.cAtts = attachments
.filter((item) => item.type === AttachmentType.COMPLAINT_ATTACHMENT)
.map((item) => {
return {
name: item.name,
date: _applyTimezone(item.date, tz, "datetime"),
fileType: getFileType(item.name),
};
});
data.hasComplaintAttachments = data.cAtts?.length > 0;

data.oAtts = attachments
.filter((item) => item.type === AttachmentType.OUTCOME_ATTACHMENT)
.map((item) => {
return {
name: item.name,
date: _applyTimezone(item.date, tz, "datetime"),
fileType: getFileType(item.name),
};
});

data.hasOutcomeAttachments = data.oAtts?.length > 0;

return data;
} catch (error) {
Expand Down
44 changes: 33 additions & 11 deletions backend/src/v1/document/document.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Logger, Param, Query, Res, UseGuards } from "@nestjs/common";
import { Body, Controller, Logger, Post, Res, UseGuards } from "@nestjs/common";
import { Response } from "express";
import { DocumentService } from "./document.service";
import { JwtRoleGuard } from "../../auth/jwtrole.guard";
Expand All @@ -9,6 +9,8 @@ import { Token } from "../../auth/decorators/token.decorator";
import { COMPLAINT_TYPE } from "../../types/models/complaints/complaint-type";
import { format } from "date-fns";
import { escape } from "escape-html";
import { ExportComplaintParameters } from "src/types/models/complaints/export-complaint-parameters";
import { Attachment, AttachmentType } from "../../types/models/general/attachment";

@UseGuards(JwtRoleGuard)
@ApiTags("document")
Expand All @@ -18,18 +20,38 @@ export class DocumentController {

constructor(private readonly service: DocumentService) {}

@Get("/export-complaint/:type")
@Post("/export-complaint")
@Roles(Role.COS_OFFICER, Role.CEEB)
async exportComplaint(
@Param("type") type: COMPLAINT_TYPE,
@Query("id") id: string,
@Query("tz") tz: string,
@Token() token,
@Res() res: Response,
): Promise<void> {
async exportComplaint(@Body() model: ExportComplaintParameters, @Token() token, @Res() res: Response): Promise<void> {
const id: string = model?.id ?? "unknown";

const complaintsAttachments = model?.attachments?.complaintsAttachments ?? [];
const outcomeAttachments = model?.attachments?.outcomeAttachments ?? [];

const attachments: Attachment[] = [
...complaintsAttachments.map((item, index) => {
return {
type: AttachmentType.COMPLAINT_ATTACHMENT,
user: item.createdBy,
name: decodeURIComponent(item.name),
date: item.createdAt,
sequenceId: index,
} as Attachment;
}),
...outcomeAttachments.map((item, index) => {
return {
type: AttachmentType.OUTCOME_ATTACHMENT,
date: item.createdAt,
name: decodeURIComponent(item.name),
user: item.createdBy,
sequenceId: index,
} as Attachment;
}),
];

try {
const fileName = `Complaint-${id}-${type}-${format(new Date(), "yyyy-MM-dd")}.pdf`;
const response = await this.service.exportComplaint(id, type, fileName, tz, token);
const fileName = `Complaint-${id}-${model.type}-${format(new Date(), "yyyy-MM-dd")}.pdf`;
const response = await this.service.exportComplaint(id, model.type, fileName, model.tz, token, attachments);

if (!response || !response.data) {
throw Error(`exception: unable to export document for complaint: ${id}`);
Expand Down
12 changes: 10 additions & 2 deletions backend/src/v1/document/document.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Inject, Injectable, Logger } from "@nestjs/common";
import { CdogsService } from "../../external_api/cdogs/cdogs.service";
import { ComplaintService } from "../complaint/complaint.service";
import { COMPLAINT_TYPE } from "../../types/models/complaints/complaint-type";
import { Attachment } from "src/types/models/general/attachment";

@Injectable()
export class DocumentService {
Expand All @@ -17,11 +18,18 @@ export class DocumentService {
//-- using the cdogs api generate a new document from the specified
//-- complaint-id and complaint type
//--
exportComplaint = async (id: string, type: COMPLAINT_TYPE, name: string, tz: string, token: string) => {
exportComplaint = async (
id: string,
type: COMPLAINT_TYPE,
name: string,
tz: string,
token: string,
attachments?: Attachment[],
) => {
try {
//-- get the complaint from the system, but do not include anything other
//-- than the base complaint. no maps, no attachments, no outcome data
const data = await this.ceds.getReportData(id, type, tz, token);
const data = await this.ceds.getReportData(id, type, tz, token, attachments);

//--
return await this.cdogs.generate(name, data, type);
Expand Down
Binary file modified backend/templates/complaint/CDOGS-CEEB-COMPLAINT-TEMPLATE-v1.docx
Binary file not shown.
Binary file not shown.
Binary file not shown.
13 changes: 8 additions & 5 deletions frontend/src/app/store/reducers/documents-thunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { format } from "date-fns";
import axios, { AxiosRequestConfig } from "axios";
import { AUTH_TOKEN, getUserAgency } from "@service/user-service";
import { AgencyType } from "@apptypes/app/agency-types";
import { ExportComplaintInput } from "@/app/types/complaints/export-complaint-input";

//--
//-- exports a complaint as a pdf document
//--
export const exportComplaint =
(type: string, id: string): ThunkAction<Promise<string | undefined>, RootState, unknown, Action<string>> =>
async (dispatch) => {
async (dispatch, getState) => {
const { attachments } = getState();
try {
const agency = getUserAgency();
let tailored_filename = "";
Expand All @@ -38,18 +40,19 @@ export const exportComplaint =
tailored_filename = `Complaint-${id}-${type}-${format(new Date(), "yyyy-MM-dd")}.pdf`;
}

const tz: string = encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone);
const tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone;

const axiosConfig: AxiosRequestConfig = {
responseType: "arraybuffer", // Specify response type as arraybuffer
};

axios.defaults.headers.common["Authorization"] = `Bearer ${localStorage.getItem(AUTH_TOKEN)}`;

const url = `${config.API_BASE_URL}/v1/document/export-complaint/${type}?id=${id}&tz=${tz}`;
const exportComplaintInput = { id, type, tz, attachments } as ExportComplaintInput;

//-- this should not work as there's no authentication token passed to the server,
const response = await axios.get(url, axiosConfig);
const url = `${config.API_BASE_URL}/v1/document/export-complaint`;

const response = await axios.post(url, exportComplaintInput, axiosConfig);

//-- this is a janky solution, but as of 2024 it is still the widly
//-- accepted solution to download a file from a service
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/app/types/complaints/export-complaint-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AttachmentsState } from "../state/attachments-state";

export interface ExportComplaintInput {
id: string;
type: string;
tz: string;
attachments: AttachmentsState;
}

0 comments on commit cf77894

Please sign in to comment.