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: CEEB Export Scripts and Export Merge Tool #832

Merged
merged 21 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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.
2 changes: 2 additions & 0 deletions charts/app/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
{{- $webeocUrl := (get $secretData "webeocUrl" | b64dec | default "") }}
{{- $webeocCronExpression := (get $secretData "webeocCronExpression" | b64dec | default "") }}
{{- $webeocLogPath := (get $secretData "webeocLogPath" | b64dec | default "") }}
{{- $webeocDateFilter := (get $secretData "webeocDateFilter" | b64dec | default "2025-01-01T08:00:00Z") }}
{{- $backupDir := (get $secretData "backupDir" | b64dec | default "") }}
{{- $backupStrategy := (get $secretData "backupStrategy" | b64dec | default "") }}
{{- $numBackups := (get $secretData "numBackups" | b64dec | default "") }}
Expand Down Expand Up @@ -139,6 +140,7 @@ data:
WEBEOC_URL: {{ $webeocUrl | b64enc | quote }}
WEBEOC_CRON_EXPRESSION: {{ $webeocCronExpression | b64enc | quote }}
WEBEOC_LOG_PATH: {{ $webeocLogPath | b64enc | quote }}
WEBEOC_DATE_FILTER: {{ $webeocDateFilter | b64enc | quote }}
COMPLAINTS_API_KEY: {{ $caseManagementApiKey | b64enc | quote }}
{{- end }}
{{- if not (lookup "v1" "Secret" .Release.Namespace (printf "%s-flyway" .Release.Name)) }}
Expand Down
56 changes: 56 additions & 0 deletions exports/ceeb_complaint_export.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-----------------------------------------------------
-- Quarterly CEEB Complaint Export query to be run for CEEB statistics
-- see https://github.com/bcgov/nr-compliance-enforcement/wiki/Data-Exports for more information
--
-- Note some extra fields are commented out of the query as these are currently part of the
-- NRIS export however they are not currently being used for reporting
-----------------------------------------------------
select
cmp.complaint_identifier as "Record ID",
TO_CHAR(((cmp.incident_reported_utc_timestmp at time zone 'UTC') at time zone 'PDT'), 'MM/DD/YYYY') as "Date Received",
CASE
WHEN cst.short_description = 'Open' THEN 'Incomplete'
WHEN cst.short_description = 'Pending Review' THEN 'Incomplete'
WHEN cst.short_description = 'Closed' THEN 'Complete'
ELSE cst.short_description
END as "Complaint Status",
cmrc.long_description as "Method Received",
--'IDIR\' || ofc.user_id as "Officer Assigned",
gfv.region_name as "Region",
--gfv.offloc_name as "Office",
goc.short_description as "City/Town",
--cmp.caller_name as "Complainant Contact",
--CASE
-- WHEN cmp.is_privacy_requested = 'Y' THEN 'Yes'
-- ELSE 'No'
--END as "Privacy Requested",
--cmp.caller_email as "Email",
--cmp.caller_phone_1 as "Telephone No.",
--cmp.location_summary_text as "Facility/Site Location",
--ST_Y(cmp.location_geometry_point) as "Latitude",
--ST_X(cmp.location_geometry_point) as "Longitude",
ac.suspect_witnesss_dtl_text as "Alleged Contravener"
from
complaint cmp
join
complaint_status_code cst on cst.complaint_status_code = cmp.complaint_status_code
join
geo_organization_unit_code goc on goc.geo_organization_unit_code = cmp.geo_organization_unit_code
join
cos_geo_org_unit_flat_vw gfv on gfv.area_code = goc.geo_organization_unit_code
left join
comp_mthd_recv_cd_agcy_cd_xref cmrcacx on cmrcacx.comp_mthd_recv_cd_agcy_cd_xref_guid = cmp.comp_mthd_recv_cd_agcy_cd_xref_guid
left join
complaint_method_received_code cmrc on cmrc.complaint_method_received_code = cmrcacx.complaint_method_received_code
left join
person_complaint_xref pcx on pcx.complaint_identifier = cmp.complaint_identifier and pcx.active_ind = true
left join
person per on per.person_guid = pcx.person_guid
left join
officer ofc on ofc.person_guid = per.person_guid
right join
allegation_complaint ac on ac.complaint_identifier = cmp.complaint_identifier
where
cmp.incident_reported_utc_timestmp >= CURRENT_DATE - INTERVAL '1 year'
order by
cmp.complaint_identifier asc
34 changes: 34 additions & 0 deletions exports/merge_exports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This script merges two CSV files based on the 'Record ID' column.
# Setup Instructions:
# 1. Ensure Python is installed and added to your system PATH.
# 2. Add the 'Scripts' directory from the Python installation to the PATH (for pip).
# 3. Install required packages by running: pip install pandas
# 4. Place 'complaints.csv' and 'cases.csv' in the same folder as this script.
# 5. Run the script using: python merge_exports.py

import pandas as pd

def main():
# Define filenames
complaint_file = "complaints.csv"
case_file = "cases.csv"
output_file = "NatCom_Export.csv"
merge_column = "Record ID" # CEEB = "Record ID" COS = "Complaint Identifier"

try:
# Load data from both files
complaint_df = pd.read_csv(complaint_file)
case_df = pd.read_csv(case_file)

# Merge data on 'Record ID' with validation
combined_df = pd.merge(complaint_df, case_df, on=merge_column, how="outer", validate="many_to_many")

# Save the merged data to a new CSV file
combined_df.to_csv(output_file, index=False, encoding='utf-8-sig')
print(f"Data successfully merged into {output_file}")

except FileNotFoundError as e:
print(f"Error: {e}\nPlease ensure both files exist in the correct directory.")

if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion frontend/src/app/common/validation-checkbox-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const ValidationCheckboxGroup: FC<ValidationCheckboxGroupProps> = ({

useEffect(() => {
setCheckedItems(checkedValues);
}, [checkedValues.length]);
}, [checkedValues, checkedValues.length]);

return (
<div id="checkbox-div">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/common/validation-phone-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const ValidationPhoneInput: FC<ValidationPhoneInputProps> = ({
international,
}) => {
const errClass = errMsg === "" ? "" : "error-message";
const calulatedClass = errMsg === "" ? "comp-form-control" : "comp-form-control" + " error-border";
const calulatedClass = errMsg === "" ? "comp-form-control" : "comp-form-control error-border";
return (
<div>
<div className={className}>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/components/common/attachments-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const AttachmentsCarousel: FC<Props> = ({
if (complaintIdentifier) {
dispatch(getAttachments(complaintIdentifier, attachmentType));
}
}, [complaintIdentifier, dispatch]);
}, [attachmentType, complaintIdentifier, dispatch]);

//-- when the component unmounts clear the attachments from redux
useEffect(() => {
Expand All @@ -82,15 +82,15 @@ export const AttachmentsCarousel: FC<Props> = ({
if (typeof onSlideCountChange === "function") {
onSlideCountChange(slides.length);
}
}, [slides.length]);
}, [onSlideCountChange, slides.length]);

// Clear all pending upload attachments
useEffect(() => {
if (cancelPendingUpload) {
setSlides([]);
if (setCancelPendingUpload) setCancelPendingUpload(false); //reset cancelPendingUpload
}
}, [cancelPendingUpload]);
}, [cancelPendingUpload, setCancelPendingUpload]);

function sortAttachmentsByName(comsObjects: COMSObject[]): COMSObject[] {
// Create a copy of the array using slice() or spread syntax
Expand Down
Loading
Loading