Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CE-1082-Add-attachment-summary-to-PDF-exports #828

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
Loading