diff --git a/.github/workflows/.dbdeployer.yml b/.github/workflows/.dbdeployer.yml index 7517562e3..e69f21960 100644 --- a/.github/workflows/.dbdeployer.yml +++ b/.github/workflows/.dbdeployer.yml @@ -5,12 +5,12 @@ on: inputs: ### Required directory: description: Crunchy Chart directory - default: 'charts/crunchy' + default: "charts/crunchy" required: false type: string oc_server: default: https://api.silver.devops.gov.bc.ca:6443 - description: 'OpenShift server' + description: "OpenShift server" required: false type: string environment: @@ -21,29 +21,29 @@ on: description: Cluster environment name, should be dev,test,prod required: false type: string - default: 'dev' + default: "dev" s3_enabled: description: Enable S3 backups required: false default: true type: boolean values: - description: 'Values file' - default: 'values.yaml' + description: "Values file" + default: "values.yaml" required: false type: string app_values: - description: 'App specific values file which is present inside charts/app' - default: 'values.yaml' + description: "App specific values file which is present inside charts/app" + default: "values.yaml" required: false type: string enabled: - description: 'Enable the deployment of the crunchy database, easy switch to turn it on/off' + description: "Enable the deployment of the crunchy database, easy switch to turn it on/off" default: true required: false type: boolean timeout-minutes: - description: 'Timeout minutes' + description: "Timeout minutes" default: 20 required: false type: number @@ -52,8 +52,8 @@ on: required: false type: string params: - description: 'Extra parameters to pass to helm upgrade' - default: '' + description: "Extra parameters to pass to helm upgrade" + default: "" required: false type: string secrets: @@ -88,7 +88,7 @@ jobs: uses: redhat-actions/openshift-tools-installer@v1 with: oc: "4.14.37" - - uses: bcgov-nr/action-diff-triggers@v0.2.0 + - uses: bcgov/action-diff-triggers@v0.2.0 id: triggers with: triggers: ${{ inputs.triggers }} @@ -146,47 +146,44 @@ jobs: shell: bash if: github.event_name == 'pull_request' run: | - echo 'Adding PR specific user to Crunchy DB' - NEW_USER='{"databases":["app-${{ github.event.number }}"],"name":"app-${{ github.event.number }}"}' - CURRENT_USERS=$(oc get PostgresCluster/postgres-crunchy-${{ inputs.cluster_environment }} -o json | jq '.spec.users') - echo "${CURRENT_USERS}" + echo 'Adding PR specific user to Crunchy DB' + NEW_USER='{"databases":["app-${{ github.event.number }}"],"name":"app-${{ github.event.number }}"}' + CURRENT_USERS=$(oc get PostgresCluster/postgres-crunchy-${{ inputs.cluster_environment }} -o json | jq '.spec.users') + echo "${CURRENT_USERS}" - # check if current_users already contains the new_user - if echo "${CURRENT_USERS}" | jq -e ".[] | select(.name == \"app-${{ github.event.number }}\")" > /dev/null; then - echo "User already exists" - exit 0 - fi - - UPDATED_USERS=$(echo "$CURRENT_USERS" | jq --argjson NEW_USER "$NEW_USER" '. + [$NEW_USER]') - echo "$UPDATED_USERS" - PATCH_JSON=$(jq -n --argjson users "$UPDATED_USERS" '{"spec": {"users": $users}}') - echo "$PATCH_JSON" - oc patch PostgresCluster/postgres-crunchy-${{ inputs.cluster_environment }} --type=merge -p "${PATCH_JSON}" - - # wait for sometime as it takes time to create the user, query the secret and check if it is created, otherwise wait in a loop for 10 rounds - for i in {1..10}; do - if oc get secret postgres-crunchy-${{ inputs.cluster_environment }}-pguser-app-${{ github.event.number }} -o jsonpath='{.metadata.name}' > /dev/null; then - echo "Secret created" - break - else - echo "Secret not created, waiting for 60 seconds" - sleep 60 - fi - done + # check if current_users already contains the new_user + if echo "${CURRENT_USERS}" | jq -e ".[] | select(.name == \"app-${{ github.event.number }}\")" > /dev/null; then + echo "User already exists" + exit 0 + fi - # Add public schema and grant to PR user - # get primary crunchy pod and remove the role and db - CRUNCHY_PG_PRIMARY_POD_NAME=$(oc get pods -l postgres-operator.crunchydata.com/role=master -o json | jq -r '.items[0].metadata.name') - echo "${CRUNCHY_PG_PRIMARY_POD_NAME}" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "CREATE SCHEMA IF NOT EXISTS public;" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON SCHEMA public TO \"app-${{ github.event.number }}\";" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"app-${{ github.event.number }}\";" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"app-${{ github.event.number }}\";" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"app-${{ github.event.number }}\";" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO \"app-${{ github.event.number }}\";" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO \"app-${{ github.event.number }}\";" - oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO \"app-${{ github.event.number }}\";" - # TODO: remove these + UPDATED_USERS=$(echo "$CURRENT_USERS" | jq --argjson NEW_USER "$NEW_USER" '. + [$NEW_USER]') + echo "$UPDATED_USERS" + PATCH_JSON=$(jq -n --argjson users "$UPDATED_USERS" '{"spec": {"users": $users}}') + echo "$PATCH_JSON" + oc patch PostgresCluster/postgres-crunchy-${{ inputs.cluster_environment }} --type=merge -p "${PATCH_JSON}" - + # wait for sometime as it takes time to create the user, query the secret and check if it is created, otherwise wait in a loop for 10 rounds + for i in {1..10}; do + if oc get secret postgres-crunchy-${{ inputs.cluster_environment }}-pguser-app-${{ github.event.number }} -o jsonpath='{.metadata.name}' > /dev/null; then + echo "Secret created" + break + else + echo "Secret not created, waiting for 60 seconds" + sleep 60 + fi + done + # Add public schema and grant to PR user + # get primary crunchy pod and remove the role and db + CRUNCHY_PG_PRIMARY_POD_NAME=$(oc get pods -l postgres-operator.crunchydata.com/role=master -o json | jq -r '.items[0].metadata.name') + echo "${CRUNCHY_PG_PRIMARY_POD_NAME}" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "CREATE SCHEMA IF NOT EXISTS public;" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON SCHEMA public TO \"app-${{ github.event.number }}\";" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"app-${{ github.event.number }}\";" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"app-${{ github.event.number }}\";" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"app-${{ github.event.number }}\";" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO \"app-${{ github.event.number }}\";" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO \"app-${{ github.event.number }}\";" + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -d "app-${{ github.event.number }}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO \"app-${{ github.event.number }}\";" + # TODO: remove these diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 3c634a375..70dd7d1c0 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -40,7 +40,7 @@ jobs: - dir: frontend token: SONAR_TOKEN_FRONTEND steps: - - uses: bcgov-nr/action-test-and-analyse@v1.2.1 + - uses: bcgov/action-test-and-analyse@v1.2.1 with: commands: | npm ci diff --git a/.github/workflows/merge-hotfix.yml b/.github/workflows/merge-hotfix.yml index 81e24bff9..de3618cc5 100644 --- a/.github/workflows/merge-hotfix.yml +++ b/.github/workflows/merge-hotfix.yml @@ -32,7 +32,7 @@ jobs: # Get PR number for squash merges to main - name: PR Number id: pr - uses: bcgov-nr/action-get-pr@v0.0.1 + uses: bcgov/action-get-pr@v0.0.1 # https://github.com/bcgov/quickstart-openshift-helpers deploy-hotfix: diff --git a/.github/workflows/pr-close.yml b/.github/workflows/pr-close.yml index f7ff62e4e..7e0783a9e 100644 --- a/.github/workflows/pr-close.yml +++ b/.github/workflows/pr-close.yml @@ -28,7 +28,7 @@ jobs: oc_token: ${{ secrets.OC_TOKEN }} with: cleanup: label - + cleanup-pvcs: name: Cleanup Project PVCs runs-on: ubuntu-22.04 @@ -60,32 +60,32 @@ jobs: oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! - run: | # check if postgres-crunchy exists or else exit - oc get PostgresCluster/postgres-crunchy || exit 0 + oc get PostgresCluster/postgres-crunchy-dev || exit 0 # Remove the user from the crunchy cluster yaml and apply the changes USER_TO_REMOVE='{"databases":["app-${{ github.event.number }}"],"name":"app-${{ github.event.number }}"}' - + echo 'getting current users from crunchy' - CURRENT_USERS=$(oc get PostgresCluster/postgres-crunchy -o json | jq '.spec.users') + CURRENT_USERS=$(oc get PostgresCluster/postgres-crunchy-dev -o json | jq '.spec.users') echo "${CURRENT_USERS}" - + # Remove the user from the list, UPDATED_USERS=$(echo "$CURRENT_USERS" | jq --argjson user "$USER_TO_REMOVE" 'map(select(. != $user))') PATCH_JSON=$(jq -n --argjson users "$UPDATED_USERS" '{"spec": {"users": $users}}') - oc patch PostgresCluster/postgres-crunchy --type=merge -p "$PATCH_JSON" - + oc patch PostgresCluster/postgres-crunchy-dev --type=merge -p "$PATCH_JSON" + # get primary crunchy pod and remove the role and db CRUNCHY_PG_PRIMARY_POD_NAME=$(oc get pods -l postgres-operator.crunchydata.com/role=master -o json | jq -r '.items[0].metadata.name') - + echo "${CRUNCHY_PG_PRIMARY_POD_NAME}" # Terminate all connections to the database before trying terminate oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'app-${{ github.event.number }}' AND pid <> pg_backend_pid();" - + # Drop the database and role oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -c "DROP DATABASE \"app-${{ github.event.number }}\" --cascade" - + oc exec "${CRUNCHY_PG_PRIMARY_POD_NAME}" -- psql -c "DROP ROLE \"app-${{ github.event.number }}\" --cascade" - + echo "Database and Role for PR is cleaned." - - exit 0 \ No newline at end of file + + exit 0 diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml index 4ae4a5a87..9d011fb53 100644 --- a/.github/workflows/pr-open.yml +++ b/.github/workflows/pr-open.yml @@ -16,7 +16,7 @@ concurrency: cancel-in-progress: true jobs: - # https://github.com/bcgov-nr/action-builder-ghcr + # https://github.com/bcgov/action-builder-ghcr builds: name: Builds runs-on: ubuntu-22.04 @@ -25,7 +25,7 @@ jobs: package: [backend, frontend, migrations, webeoc] timeout-minutes: 10 steps: - - uses: bcgov-nr/action-builder-ghcr@v2.2.0 + - uses: bcgov/action-builder-ghcr@v2.2.0 with: package: ${{ matrix.package }} tag: ${{ github.event.number }} diff --git a/backend/dataloader/bulk-data-loader.js b/backend/dataloader/bulk-data-loader.js index ccad9bfd5..7a55a92c5 100644 --- a/backend/dataloader/bulk-data-loader.js +++ b/backend/dataloader/bulk-data-loader.js @@ -248,16 +248,69 @@ const insertData = async (data) => { } }; -// Adjust these as required. -// No more than 10k at a time or the insert will blow up. -const yearPrefix = 25; -const numRecords = 10000; -const startingRecord = 100000; - -// Validate parameters -if (numRecords > 10000) { - console.log ("Please adjust the numRecords parameter to be less than 10000"); - return; -} -const records = generateBulkData(yearPrefix, numRecords, startingRecord); -insertData(records); +// Process any pending complaints +const processPendingComplaints = async () => { + const query = `DO $$ + DECLARE + complaint_record RECORD; + BEGIN + -- Loop through each pending complaint + FOR complaint_record IN + SELECT complaint_identifier + FROM staging_complaint + WHERE staging_status_code = 'PENDING' + LOOP + -- Call the insert_complaint_from_staging function for each complaint + PERFORM public.insert_complaint_from_staging(complaint_record.complaint_identifier); + END LOOP; + + TRUNCATE TABLE staging_complaint; + + -- Optional: Add a message indicating completion + RAISE NOTICE 'All pending complaints processed successfully.'; + END; + $$;`; + + try { + // Perform the bulk insert + await pg.none(query); + console.log('Data processing complete.'); + } catch (error) { + console.error('Error processing data:', error); + } +}; + +const bulkDataLoad = async () => { + + // Adjust these as required. + const processAfterInsert = true // Will move complaints from staging to the live table after each iteration + const numRecords = 10000; // Records created per iteration, no more than 10k at a time or the insert will blow up. + const yearPrefix = 20; // Year will increment per iteration + const startingRecord = 110000; + const iterations = 10; + + // Validate parameters + if (numRecords > 10000) { + console.log ("Please adjust the numRecords parameter to be less than 10000"); + return; + } + if (iterations * numRecords > 1000000) { + console.log ("Please adjust the iterations so that fewer than 1000000 records are inserted"); + return; + } + + for (let i = 0; i < iterations; i++) { + const startingRecordForIteration = startingRecord + (i * numRecords); + const records = generateBulkData(yearPrefix + i, numRecords, startingRecordForIteration); + // Insert the staging data + await insertData(records); + // Process the staging data + if (processAfterInsert) + { + await processPendingComplaints(); + } + console.log(`Inserted records ${startingRecordForIteration} through ${startingRecordForIteration + numRecords}.`); + } +}; + +bulkDataLoad(); diff --git a/backend/package-lock.json b/backend/package-lock.json index d86579e13..4b8fecfb4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "@automapper/types": "^6.3.1", "@golevelup/ts-jest": "^0.4.0", "@nestjs/axios": "^3.0.3", + "@nestjs/cache-manager": "^2.3.0", "@nestjs/cli": "^10.4.5", "@nestjs/common": "^10.4.4", "@nestjs/config": "^3.2.3", @@ -30,6 +31,7 @@ "@nestjs/typeorm": "^10.0.2", "automapper": "^1.0.0", "axios": "^1.7.4", + "cache-manager": "^5.7.6", "cron": "^3.1.7", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", @@ -1541,6 +1543,17 @@ "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.3.0.tgz", + "integrity": "sha512-pxeBp9w/s99HaW2+pezM1P3fLiWmUEnTUoUMLa9UYViCtjj0E0A19W/vaT5JFACCzFIeNrwH4/16jkpAhQ25Vw==", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz", @@ -3313,6 +3326,25 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", + "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5017,6 +5049,11 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -8844,6 +8881,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "engines": { + "node": ">=16" + } + }, "node_modules/promise-polyfill": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", @@ -12184,6 +12229,12 @@ "integrity": "sha512-h6TCn3yJwD6OKqqqfmtRS5Zo4E46Ip2n+gK1sqwzNBC+qxQ9xpCu+ODVRFur6V3alHSCSBxb3nNtt73VEdluyA==", "requires": {} }, + "@nestjs/cache-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.3.0.tgz", + "integrity": "sha512-pxeBp9w/s99HaW2+pezM1P3fLiWmUEnTUoUMLa9UYViCtjj0E0A19W/vaT5JFACCzFIeNrwH4/16jkpAhQ25Vw==", + "requires": {} + }, "@nestjs/cli": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz", @@ -13496,6 +13547,24 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cache-manager": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", + "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", + "requires": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + } + } + }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -14728,6 +14797,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -17531,6 +17605,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==" + }, "promise-polyfill": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", diff --git a/backend/package.json b/backend/package.json index 2ccc84dda..88f005f43 100644 --- a/backend/package.json +++ b/backend/package.json @@ -47,6 +47,7 @@ "@automapper/types": "^6.3.1", "@golevelup/ts-jest": "^0.4.0", "@nestjs/axios": "^3.0.3", + "@nestjs/cache-manager": "^2.3.0", "@nestjs/cli": "^10.4.5", "@nestjs/common": "^10.4.4", "@nestjs/config": "^3.2.3", @@ -61,6 +62,7 @@ "@nestjs/typeorm": "^10.0.2", "automapper": "^1.0.0", "axios": "^1.7.4", + "cache-manager": "^5.7.6", "cron": "^3.1.7", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", diff --git a/backend/src/auth/jwtrole.guard.ts b/backend/src/auth/jwtrole.guard.ts index 365959941..10f44c0d6 100644 --- a/backend/src/auth/jwtrole.guard.ts +++ b/backend/src/auth/jwtrole.guard.ts @@ -58,7 +58,7 @@ export class JwtRoleGuard extends AuthGuard("jwt") implements CanActivate { } const userRoles: string[] = user.client_roles; // Check if the user has the readonly role - const hasReadOnlyRole = userRoles.includes(Role.READ_ONLY); + const hasReadOnlyRole = userRoles?.includes(Role.READ_ONLY); // If the user has readonly role, allow only GET requests unless the route is in the list of exceptions if (hasReadOnlyRole && !READ_ONLY_EXCEPTIONS.includes(request.route.path)) { diff --git a/backend/src/external_api/css/css.module.ts b/backend/src/external_api/css/css.module.ts index 7b3ad6a9d..1a3c13760 100644 --- a/backend/src/external_api/css/css.module.ts +++ b/backend/src/external_api/css/css.module.ts @@ -1,9 +1,10 @@ import { Module } from "@nestjs/common"; import { CssService } from "./css.service"; import { ConfigurationModule } from "../../v1/configuration/configuration.module"; +import { CacheModule } from "@nestjs/cache-manager"; @Module({ - imports: [ConfigurationModule], + imports: [ConfigurationModule, CacheModule.register()], providers: [CssService], exports: [CssService], }) diff --git a/backend/src/external_api/css/css.service.spec.ts b/backend/src/external_api/css/css.service.spec.ts index 0ae63760d..36484ea4d 100644 --- a/backend/src/external_api/css/css.service.spec.ts +++ b/backend/src/external_api/css/css.service.spec.ts @@ -3,12 +3,14 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import { ConfigurationService } from "../../v1/configuration/configuration.service"; import { Configuration } from "../../v1/configuration/entities/configuration.entity"; import { CssService } from "./css.service"; +import { CacheModule } from "@nestjs/cache-manager"; describe("CssService", () => { let service: CssService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [CacheModule.register()], providers: [ CssService, ConfigurationService, diff --git a/backend/src/external_api/css/css.service.ts b/backend/src/external_api/css/css.service.ts index 01e28b6d7..7e454527e 100644 --- a/backend/src/external_api/css/css.service.ts +++ b/backend/src/external_api/css/css.service.ts @@ -4,6 +4,8 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { get } from "../../helpers/axios-api"; import { ConfigurationService } from "../../v1/configuration/configuration.service"; import { CssUser } from "src/types/css/cssUser"; +import { CACHE_MANAGER } from "@nestjs/cache-manager"; +import { Cache } from "cache-manager"; @Injectable() export class CssService implements ExternalApiService { @@ -20,6 +22,9 @@ export class CssService implements ExternalApiService { @Inject(ConfigurationService) readonly configService: ConfigurationService; + @Inject(CACHE_MANAGER) + readonly cacheManager: Cache; + constructor() { this.authApi = process.env.CSS_TOKEN_URL; this.baseUri = process.env.CSS_URL; @@ -112,6 +117,10 @@ export class CssService implements ExternalApiService { }, }; const response = await axios.post(url, userRoles, config); + + // clear cache + await this.clearUserRoleCache(); + return response?.data.data; } catch (error) { this.logger.error(`exception: unable to update user's roles ${userIdir} - error: ${error}`); @@ -130,6 +139,10 @@ export class CssService implements ExternalApiService { }, }; const response = await axios.delete(url, config); + + // clear cache + await this.clearUserRoleCache(); + return response?.data.data; } catch (error) { this.logger.error(`exception: unable to delete user's role ${userIdir} - error: ${error}`); @@ -137,69 +150,110 @@ export class CssService implements ExternalApiService { } }; - getUserRoleMapping = async (): Promise => { - try { - const apiToken = await this.authenticate(); - //Get all roles from NatCom CSS integation - const rolesUrl = `${this.baseUri}/api/v1/integrations/4794/${this.env}/roles`; - const config: AxiosRequestConfig = { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiToken}`, - }, - }; - const roleRes = await get(apiToken, rolesUrl, config); - if (roleRes?.data.data.length > 0) { - const { - data: { data: roleList }, - } = roleRes; - - //Get all users for each role - let usersUrl: string = ""; - const pages = Array.from(Array(this.maxPages), (_, i) => i + 1); - const usersRoles = await Promise.all( - roleList.map(async (role) => { - let usersRolesTemp = []; - for (const page of pages) { - usersUrl = `${this.baseUri}/api/v1/integrations/4794/${this.env}/roles/${role.name}/users?page=${page}`; - const userRes = await get(apiToken, encodeURI(usersUrl), config); - if (userRes?.data.data.length > 0) { - const { - data: { data: users }, - } = userRes; - let usersRolesSinglePage = await Promise.all( - users.map((user) => { - return { - userId: user.username - .replace(/@idir$/i, "") - .replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5"), - role: role.name, - }; - }), - ); - usersRolesTemp.push(...usersRolesSinglePage); - } else { - break; - } + private readonly clearUserRoleCache = async (): Promise => { + await this.cacheManager.del("css-users-roles-fresh"); + await this.cacheManager.del("css-users-roles-stale"); + await this.cacheManager.del("css-users-roles-refresh-status"); + }; + + private readonly fetchAndGroupUserRoles = async (): Promise => { + //Try to avoid multiple refreshes of cache in case of multiple requests while the cache is being refreshed + await this.cacheManager.set("css-users-roles-refresh-status", true, 15000); + + const apiToken = await this.authenticate(); + //Get all roles from NatCom CSS integration + const rolesUrl = `${this.baseUri}/api/v1/integrations/4794/${this.env}/roles`; + const config: AxiosRequestConfig = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiToken}`, + }, + }; + const roleRes = await get(apiToken, rolesUrl, config); + if (roleRes?.data.data.length > 0) { + const { + data: { data: roleList }, + } = roleRes; + + //Get all users for each role + let usersUrl: string = ""; + const pages = Array.from(Array(this.maxPages), (_, i) => i + 1); + const usersRoles = await Promise.all( + roleList.map(async (role) => { + let usersRolesTemp = []; + for (const page of pages) { + usersUrl = `${this.baseUri}/api/v1/integrations/4794/${this.env}/roles/${role.name}/users?page=${page}`; + const userRes = await get(apiToken, encodeURI(usersUrl), config); + if (userRes?.data.data.length > 0) { + const { + data: { data: users }, + } = userRes; + let usersRolesSinglePage = await Promise.all( + users.map((user) => { + return { + userId: user.username + .replace(/@idir$/i, "") + .replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5"), + role: role.name, + }; + }), + ); + usersRolesTemp.push(...usersRolesSinglePage); + } else { + break; } - return usersRolesTemp; - }), - ); + } + return usersRolesTemp; + }), + ); - //exclude empty roles and concatenate all sub-array elements - const usersRolesFlat = usersRoles.filter((item) => item !== undefined).flat(); + //exclude empty roles and concatenate all sub-array elements + const usersRolesFlat = usersRoles.filter((item) => item !== undefined).flat(); - //group the array elements by a user id - const usersRolesGroupped = usersRolesFlat.reduce((grouping, item) => { - grouping[item.userId] = [...(grouping[item.userId] || []), item.role]; - return grouping; - }, {}); + //group the array elements by a user id + const usersRolesGrouped = usersRolesFlat.reduce((grouping, item) => { + grouping[item.userId] = [...(grouping[item.userId] || []), item.role]; + return grouping; + }, {}); - return usersRolesGroupped; + //set the fresh cache for 10 minutes + await this.cacheManager.set("css-users-roles-fresh", usersRolesGrouped, 600000); + + //set the stale cache for 60 minutes + await this.cacheManager.set("css-users-roles-stale", usersRolesGrouped, 3600000); + + return usersRolesGrouped; + } else { + throw new Error(`unable to get user roles from CSS or no roles found`); + } + }; + + getUserRoleMapping = async (): Promise => { + try { + // return fresh cache if available + const usersRolesGroupedFresh = await this.cacheManager.get("css-users-roles-fresh"); + if (usersRolesGroupedFresh) { + return usersRolesGroupedFresh; } + + // return the stale cache if available, but asyncronously refresh cache + const usersRolesGroupedStale = await this.cacheManager.get("css-users-roles-stale"); + if (usersRolesGroupedStale) { + // check if a refresh is already in progress, if not start a new refresh + const refreshingInProgress = await this.cacheManager.get("css-users-roles-refresh-status"); + if (!refreshingInProgress) { + this.fetchAndGroupUserRoles().catch((error) => { + this.logger.error(`exception: error: ${error}`); + }); + } + + return usersRolesGroupedStale; + } + + // wait for fresh data if no cache is available + return await this.fetchAndGroupUserRoles(); } catch (error) { this.logger.error(`exception: error: ${error}`); - return; } }; } diff --git a/backend/src/middleware/maps/automapper-dto-to-entity-maps.ts b/backend/src/middleware/maps/automapper-dto-to-entity-maps.ts index f32289983..bc71f7e15 100644 --- a/backend/src/middleware/maps/automapper-dto-to-entity-maps.ts +++ b/backend/src/middleware/maps/automapper-dto-to-entity-maps.ts @@ -153,6 +153,10 @@ export const mapComplaintDtoToComplaint = (mapper: Mapper) => { (dest) => dest.is_privacy_requested, mapFrom((src) => src.isPrivacyRequested), ), + forMember( + (dest) => dest.comp_last_upd_utc_timestamp, + mapFrom((src) => src.updatedOn), + ), ); }; @@ -341,6 +345,10 @@ export const mapWildlifeComplaintDtoToHwcrComplaint = (mapper: Mapper) => { (dest) => dest.complaint_identifier.is_privacy_requested, mapFrom((src) => src.isPrivacyRequested), ), + forMember( + (dest) => dest.complaint_identifier.comp_last_upd_utc_timestamp, + mapFrom((src) => src.updatedOn), + ), ); }; @@ -502,6 +510,10 @@ export const mapAllegationComplaintDtoToAllegationComplaint = (mapper: Mapper) = (dest) => dest.complaint_identifier.is_privacy_requested, mapFrom((src) => src.isPrivacyRequested), ), + forMember( + (dest) => dest.complaint_identifier.comp_last_upd_utc_timestamp, + mapFrom((src) => src.updatedOn), + ), ); }; @@ -609,6 +621,10 @@ export const mapGirComplaintDtoToGirComplaint = (mapper: Mapper) => { (dest) => dest.complaint_identifier.is_privacy_requested, mapFrom((src) => src.isPrivacyRequested), ), + forMember( + (dest) => dest.complaint_identifier.comp_last_upd_utc_timestamp, + mapFrom((src) => src.updatedOn), + ), ); }; diff --git a/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts b/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts index 20f3115a4..b2e27e4e7 100644 --- a/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts +++ b/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts @@ -264,6 +264,10 @@ export const complaintToComplaintDtoMap = (mapper: Mapper) => { (destination) => destination.isPrivacyRequested, mapFrom((source) => source.is_privacy_requested), ), + forMember( + (destination) => destination.updatedOn, + mapFrom((source) => source.comp_last_upd_utc_timestamp), + ), ); }; @@ -722,6 +726,10 @@ export const applyWildlifeComplaintMap = (mapper: Mapper) => { (destination) => destination.isPrivacyRequested, mapFrom((source) => source.complaint_identifier.is_privacy_requested), ), + forMember( + (destination) => destination.updatedOn, + mapFrom((source) => source.complaint_identifier.comp_last_upd_utc_timestamp), + ), ); }; @@ -949,6 +957,10 @@ export const applyAllegationComplaintMap = (mapper: Mapper) => { (destination) => destination.isPrivacyRequested, mapFrom((source) => source.complaint_identifier.is_privacy_requested), ), + forMember( + (destination) => destination.updatedOn, + mapFrom((source) => source.complaint_identifier.comp_last_upd_utc_timestamp), + ), ); }; export const applyGeneralInfomationComplaintMap = (mapper: Mapper) => { @@ -1152,6 +1164,10 @@ export const applyGeneralInfomationComplaintMap = (mapper: Mapper) => { (destination) => destination.isPrivacyRequested, mapFrom((source) => source.complaint_identifier.is_privacy_requested), ), + forMember( + (destination) => destination.updatedOn, + mapFrom((source) => source.complaint_identifier.comp_last_upd_utc_timestamp), + ), ); }; @@ -1519,7 +1535,7 @@ export const mapAllegationReport = (mapper: Mapper, tz: string = "America/Vancou ), forMember( (destination) => destination.updatedOn, - mapFrom((source) => source.complaint_identifier.update_utc_timestamp), + mapFrom((source) => source.complaint_identifier.comp_last_upd_utc_timestamp), ), forMember( (destination) => destination.officerAssigned, @@ -1732,7 +1748,6 @@ export const mapAllegationReport = (mapper: Mapper, tz: string = "America/Vancou } }), ), - //-- forMember( (destination) => destination.violationType, diff --git a/backend/src/middleware/maps/dto-to-table-map.ts b/backend/src/middleware/maps/dto-to-table-map.ts index 539f3de64..b26c258e1 100644 --- a/backend/src/middleware/maps/dto-to-table-map.ts +++ b/backend/src/middleware/maps/dto-to-table-map.ts @@ -112,6 +112,12 @@ export const mapComplaintDtoToComplaintTable = (mapper: Mapper) => { return src.isPrivacyRequested; }), ), + forMember( + (dest) => dest.comp_last_upd_utc_timestamp, + mapFrom((src) => { + return src.updatedOn; + }), + ), ); }; diff --git a/backend/src/types/models/case-files/equipment/delete-equipment-dto.ts b/backend/src/types/models/case-files/equipment/delete-equipment-dto.ts index 564a24341..24686c5cc 100644 --- a/backend/src/types/models/case-files/equipment/delete-equipment-dto.ts +++ b/backend/src/types/models/case-files/equipment/delete-equipment-dto.ts @@ -1,4 +1,5 @@ export interface DeleteEquipmentDto { id: string; updateUserId: string; + leadIdentifier: string; } diff --git a/backend/src/types/models/complaints/update-complaint.dto.ts b/backend/src/types/models/complaints/update-complaint.dto.ts index 6a28d5e11..a3858ea9e 100644 --- a/backend/src/types/models/complaints/update-complaint.dto.ts +++ b/backend/src/types/models/complaints/update-complaint.dto.ts @@ -179,4 +179,11 @@ export class UpdateComplaintDto { "flag to represent that the caller has asked for special care when handling their personal information", }) is_privacy_requested: string; + + @ApiProperty({ + example: "true", + description: + "The time the complaint was last updated, or null if the complaint has never been touched. This value might also be updated by business logic that touches sub-tables to indicate that the business object complaint has been updated.", + }) + comp_last_upd_utc_timestamp: Date; } diff --git a/backend/src/v1/case_file/case_file.controller.ts b/backend/src/v1/case_file/case_file.controller.ts index 8586aba84..68c4b6923 100644 --- a/backend/src/v1/case_file/case_file.controller.ts +++ b/backend/src/v1/case_file/case_file.controller.ts @@ -61,10 +61,12 @@ export class CaseFileController { @Token() token, @Query("id") id: string, @Query("updateUserId") userId: string, + @Query("leadIdentifier") leadIdentifier: string, ): Promise { const deleteEquipment = { id: id, updateUserId: userId, + leadIdentifier: leadIdentifier, }; return await this.service.deleteEquipment(token, deleteEquipment); } @@ -128,12 +130,14 @@ export class CaseFileController { async deleteNote( @Token() token, @Query("caseIdentifier") caseIdentifier: string, + @Query("leadIdentifier") leadIdentifier: string, @Query("actor") actor: string, @Query("updateUserId") updateUserId: string, @Query("actionId") actionId: string, ): Promise { const input = { caseIdentifier, + leadIdentifier, actor, updateUserId, actionId, @@ -159,12 +163,14 @@ export class CaseFileController { async deleteWildlife( @Token() token, @Query("caseIdentifier") caseIdentifier: string, + @Query("leadIdentifier") leadIdentifier: string, @Query("actor") actor: string, @Query("updateUserId") updateUserId: string, @Query("outcomeId") outcomeId: string, ): Promise { const input = { caseIdentifier, + leadIdentifier, actor, updateUserId, wildlifeId: outcomeId, @@ -213,11 +219,13 @@ export class CaseFileController { async deleteAuthorizationOutcome( @Token() token, @Query("caseIdentifier") caseIdentifier: string, + @Query("leadIdentifier") leadIdentifier: string, @Query("updateUserId") updateUserId: string, @Query("id") id: string, ): Promise { const input = { caseIdentifier, + leadIdentifier, updateUserId, id, }; diff --git a/backend/src/v1/case_file/case_file.service.spec.ts b/backend/src/v1/case_file/case_file.service.spec.ts index a0355b4f5..4b1bafc70 100644 --- a/backend/src/v1/case_file/case_file.service.spec.ts +++ b/backend/src/v1/case_file/case_file.service.spec.ts @@ -76,13 +76,14 @@ import { TeamService } from "../team/team.service"; import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; import { Team } from "../team/entities/team.entity"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { CacheModule } from "@nestjs/cache-manager"; describe("Testing: Case File Service", () => { let service: CaseFileService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule], + imports: [AutomapperModule, CacheModule.register()], providers: [ AutomapperModule, { diff --git a/backend/src/v1/case_file/case_file.service.ts b/backend/src/v1/case_file/case_file.service.ts index 2688d0c13..3f50192bb 100644 --- a/backend/src/v1/case_file/case_file.service.ts +++ b/backend/src/v1/case_file/case_file.service.ts @@ -188,7 +188,7 @@ export class CaseFileService { query: query, variables: model, }); - returnValue = await this.handleAPIResponse(result); + returnValue = await this.handleAPIResponse(result, complaintBeingLinkedId); // If the mutation succeeded, commit the pending transaction await queryRunner.commitTransaction(); } catch (err) { @@ -229,7 +229,8 @@ export class CaseFileService { query: query, variables: model, }); - returnValue = await this.handleAPIResponse(result); + + returnValue = await this.handleAPIResponse(result, modelAsAny.createAssessmentInput.leadIdentifier); } return returnValue?.createAssessment; @@ -283,7 +284,7 @@ export class CaseFileService { query: query, variables: model, }); - returnValue = await this.handleAPIResponse(result); + returnValue = await this.handleAPIResponse(result, modelAsAny.updateAssessmentInput.leadIdentifier); } return returnValue?.updateAssessment; }; @@ -296,7 +297,14 @@ export class CaseFileService { }`, variables: model, }); - const returnValue = await this.handleAPIResponse(result); + + // The model reaches this function in the shape { "reviewInput": {...CaseFlieDTO} } despite that property + // not existing in the CaseFileDTO type, which renders the CaseFile fields inside inaccessible in this scope. + // For example, leadIdentifier would be found in model.leadIdentifier by the type's definition, however in this + // scope it is at model.reviewInput.leadIdentifier, which errors due to type violation. + // This copies it into a new variable cast to any to allow access to the nested properties. + let modelAsAny: any = { ...model }; + const returnValue = await this.handleAPIResponse(result, modelAsAny.reviewInput.leadIdentifier); const caseFileDTO = returnValue.createReview as CaseFileDto; try { if (caseFileDTO.isReviewRequired) { @@ -319,7 +327,13 @@ export class CaseFileService { }`, variables: model, }); - const returnValue = await this.handleAPIResponse(result); + // The model reaches this function in the shape { "reviewInput": {...CaseFlieDTO} } despite that property + // not existing in the CaseFileDTO type, which renders the CaseFile fields inside inaccessible in this scope. + // For example, leadIdentifier would be found in model.leadIdentifier by the type's definition, however in this + // scope it is at model.reviewInput.leadIdentifier, which errors due to type violation. + // This copies it into a new variable cast to any to allow access to the nested properties. + let modelAsAny: any = { ...model }; + const returnValue = await this.handleAPIResponse(result, modelAsAny.reviewInput.leadIdentifier); const caseFileDTO = returnValue.updateReview as CaseFileDto; try { if (model.reviewInput.isReviewRequired) { @@ -348,7 +362,13 @@ export class CaseFileService { }`, variables: model, }); - const returnValue = await this.handleAPIResponse(result); + // The model reaches this function in the shape { "createPreventionInput": {...CaseFlieDTO} } despite that property + // not existing in the CaseFileDTO type, which renders the CaseFile fields inside inaccessible in this scope. + // For example, leadIdentifier would be found in model.leadIdentifier by the type's definition, however in this + // scope it is at model.createPreventionInput.leadIdentifier, which errors due to type violation. + // This copies it into a new variable cast to any to allow access to the nested properties. + let modelAsAny: any = { ...model }; + const returnValue = await this.handleAPIResponse(result, modelAsAny.createPreventionInput.leadIdentifier); return returnValue?.createPrevention; }; @@ -360,13 +380,27 @@ export class CaseFileService { }`, variables: model, }); - const returnValue = await this.handleAPIResponse(result); + // The model reaches this function in the shape { "updatePreventionInput": {...CaseFlieDTO} } despite that property + // not existing in the CaseFileDTO type, which renders the CaseFile fields inside inaccessible in this scope. + // For example, leadIdentifier would be found in model.leadIdentifier by the type's definition, however in this + // scope it is at model.updatePreventionInput.leadIdentifier, which errors due to type violation. + // This copies it into a new variable cast to any to allow access to the nested properties. + let modelAsAny: any = { ...model }; + const returnValue = await this.handleAPIResponse(result, modelAsAny.updatePreventionInput.leadIdentifier); return returnValue?.updatePrevention; }; - private handleAPIResponse = async (result: { response: AxiosResponse; error: AxiosError }): Promise => { + private readonly handleAPIResponse = async ( + result: { response: AxiosResponse; error: AxiosError }, + leadIdentifer: string, + ): Promise => { if (result?.response?.data?.data) { + // As per CE-1335 whenever the case data is updated we want to update the last updated date on the complaint table. + // All Case Actions should call this method so this should work here. const caseFileDto = result.response.data.data; + if (leadIdentifer) { + await this.complaintService.updateComplaintLastUpdatedDate(leadIdentifer); + } return caseFileDto; } else if (result?.response?.data?.errors) { this.logger.error(`Error occurred. ${JSON.stringify(result.response.data.errors)}`); @@ -391,8 +425,13 @@ export class CaseFileService { this.logger.debug(mutationQuery); const result = await post(token, mutationQuery); - - const returnValue = await this.handleAPIResponse(result); + // The model reaches this function in the shape { "createEquipmentInput": {...CaseFlieDTO} } despite that property + // not existing in the CaseFileDTO type, which renders the CaseFile fields inside inaccessible in this scope. + // For example, leadIdentifier would be found in model.leadIdentifier by the type's definition, however in this + // scope it is at model.createEquipmentInput.leadIdentifier, which errors due to type violation. + // This copies it into a new variable cast to any to allow access to the nested properties. + let modelAsAny: any = { ...model }; + const returnValue = await this.handleAPIResponse(result, modelAsAny.createEquipmentInput.leadIdentifier); return returnValue?.createEquipment; }; @@ -404,20 +443,28 @@ export class CaseFileService { }`, variables: model, }); - const returnValue = await this.handleAPIResponse(result); + // The model reaches this function in the shape { "updateEquipmentInput": {...CaseFlieDTO} } despite that property + // not existing in the CaseFileDTO type, which renders the CaseFile fields inside inaccessible in this scope. + // For example, leadIdentifier would be found in model.leadIdentifier by the type's definition, however in this + // scope it is at model.updateEquipmentInput.leadIdentifier, which errors due to type violation. + // This copies it into a new variable cast to any to allow access to the nested properties. + let modelAsAny: any = { ...model }; + const returnValue = await this.handleAPIResponse(result, modelAsAny.updateEquipmentInput.leadIdentifier); return returnValue?.updateEquipment; }; deleteEquipment = async (token: string, model: DeleteEquipmentDto): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation DeleteEquipment($deleteEquipmentInput: DeleteEquipmentInput!) { - deleteEquipment(deleteEquipmentInput: $deleteEquipmentInput) + deleteEquipment(deleteEquipmentInput: $deleteEquipmentInput) }`, variables: { deleteEquipmentInput: model, // Ensure that the key matches the name of the variable in your mutation }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue; }; @@ -432,11 +479,13 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, model.leadIdentifier); return returnValue?.createNote; }; updateNote = async (token: any, model: UpdateSupplementalNotesInput): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation UpdateNote($input: UpdateSupplementalNoteInput!) { updateNote(input: $input) { @@ -447,11 +496,13 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue?.updateNote; }; deleteNote = async (token: any, model: DeleteSupplementalNotesInput): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation DeleteNote($input: DeleteSupplementalNoteInput!) { deleteNote(input: $input) { @@ -461,8 +512,7 @@ export class CaseFileService { }`, variables: { input: model }, }); - - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue?.deleteNote; }; @@ -476,11 +526,13 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, model.leadIdentifier); return returnValue?.createWildlife; }; updateWildlife = async (token: any, model: UpdateWildlifeInput): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation updateWildlife($input: UpdateWildlifeInput!) { updateWildlife(input: $input) { @@ -490,11 +542,13 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue?.updateWildlife; }; deleteWildlife = async (token: any, model: DeleteWildlifeInput): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation DeleteWildlife($input: DeleteWildlifeInput!) { deleteWildlife(input: $input) { @@ -504,7 +558,7 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue?.deleteWildlife; }; @@ -526,11 +580,13 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadId); return returnValue?.createDecision; }; updateDecision = async (token: any, model: UpdateDecisionInput): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation updateDecision($input: UpdateDecisionInput!) { updateDecision(input: $input) { @@ -541,7 +597,7 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue?.updateDecision; }; @@ -556,11 +612,13 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, model.leadIdentifier); return returnValue?.createAuthorizationOutcome; }; updateAuthorizationOutcome = async (token: any, model: UpdateAuthorizationOutcomeInput): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation updateAuthorizationOutcome($input: UpdateAuthorizationOutcomeInput!) { updateAuthorizationOutcome(input: $input) { @@ -571,11 +629,13 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue?.updateAuthorizationOutcome; }; deleteAuthorizationOutcome = async (token: any, model: DeleteAuthorizationOutcomeInput): Promise => { + const leadIdentifier = model.leadIdentifier; + delete model.leadIdentifier; const result = await post(token, { query: `mutation deleteAuthorizationOutcome($input: DeleteAuthorizationOutcomeInput!) { deleteAuthorizationOutcome(input: $input) { @@ -586,7 +646,7 @@ export class CaseFileService { variables: { input: model }, }); - const returnValue = await this.handleAPIResponse(result); + const returnValue = await this.handleAPIResponse(result, leadIdentifier); return returnValue?.deleteAuthorizationOutcome; }; } diff --git a/backend/src/v1/code-table/code-table.service.spec.ts b/backend/src/v1/code-table/code-table.service.spec.ts index c92c4395e..52f24c947 100644 --- a/backend/src/v1/code-table/code-table.service.spec.ts +++ b/backend/src/v1/code-table/code-table.service.spec.ts @@ -177,7 +177,7 @@ describe("Testing: CodeTable Service", () => { //-- assert expect(results).not.toBe(null); expect(results.length).not.toBe(0); - expect(results.length).toBe(6); + expect(results.length).toBe(5); }); it("should return collection of organization unit types", async () => { diff --git a/backend/src/v1/complaint/complaint.controller.ts b/backend/src/v1/complaint/complaint.controller.ts index b2ec50888..bf9c521d9 100644 --- a/backend/src/v1/complaint/complaint.controller.ts +++ b/backend/src/v1/complaint/complaint.controller.ts @@ -108,6 +108,12 @@ export class ComplaintController { return await this.service.updateComplaintById(id, complaintType, model); } + @Patch("/update-date-by-id/:id") + @Roles(Role.COS, Role.CEEB) + async updateComplaintLastUpdatedDateById(@Param("id") id: string): Promise { + return await this.service.updateComplaintLastUpdatedDate(id); + } + @Get("/by-complaint-identifier/:complaintType/:id") @Roles(Role.COS, Role.CEEB) async findComplaintById( diff --git a/backend/src/v1/complaint/complaint.service.spec.ts b/backend/src/v1/complaint/complaint.service.spec.ts index b81827bc8..03a06e9a8 100644 --- a/backend/src/v1/complaint/complaint.service.spec.ts +++ b/backend/src/v1/complaint/complaint.service.spec.ts @@ -86,13 +86,14 @@ import { TeamService } from "../team/team.service"; import { Team } from "../team/entities/team.entity"; import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { CacheModule } from "@nestjs/cache-manager"; describe("Testing: Complaint Service", () => { let service: ComplaintService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule], + imports: [AutomapperModule, CacheModule.register()], providers: [ AutomapperModule, { @@ -395,7 +396,7 @@ describe("Testing: Complaint Service", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule], + imports: [AutomapperModule, CacheModule.register()], providers: [ AutomapperModule, { diff --git a/backend/src/v1/complaint/complaint.service.ts b/backend/src/v1/complaint/complaint.service.ts index f23832606..942547665 100644 --- a/backend/src/v1/complaint/complaint.service.ts +++ b/backend/src/v1/complaint/complaint.service.ts @@ -177,6 +177,7 @@ export class ComplaintService { private readonly _generateMapQueryBuilder = ( type: COMPLAINT_TYPE, includeCosOrganization: boolean, + count: boolean, ): SelectQueryBuilder => { let builder: SelectQueryBuilder; @@ -185,14 +186,12 @@ export class ComplaintService { builder = this._allegationComplaintRepository .createQueryBuilder("allegation") .leftJoin("allegation.complaint_identifier", "complaint") - .addSelect(["complaint.complaint_identifier", "complaint.location_geometry_point"]) .leftJoin("allegation.violation_code", "violation_code"); break; case "GIR": builder = this._girComplaintRepository .createQueryBuilder("general") .leftJoin("general.complaint_identifier", "complaint") - .addSelect(["complaint.complaint_identifier", "complaint.location_geometry_point"]) .leftJoin("general.gir_type_code", "gir"); break; case "HWCR": @@ -200,13 +199,23 @@ export class ComplaintService { builder = this._wildlifeComplaintRepository .createQueryBuilder("wildlife") .leftJoin("wildlife.complaint_identifier", "complaint") - .addSelect(["complaint.complaint_identifier", "complaint.location_geometry_point"]) .leftJoin("wildlife.species_code", "species_code") .leftJoin("wildlife.hwcr_complaint_nature_code", "complaint_nature_code") .leftJoin("wildlife.attractant_hwcr_xref", "attractants", "attractants.active_ind = true") .leftJoin("attractants.attractant_code", "attractant_code"); break; } + + if (count) { + builder.select("COUNT(DISTINCT complaint.complaint_identifier)", "count"); + } else { + builder + .select("complaint.complaint_identifier", "complaint_identifier") + .distinctOn(["complaint.complaint_identifier"]) + .groupBy("complaint.complaint_identifier") + .addSelect("complaint.location_geometry_point", "location_geometry_point"); + } + builder .leftJoin("complaint.complaint_status_code", "complaint_status") .leftJoin("complaint.reported_by_code", "reported_by") @@ -232,10 +241,6 @@ export class ComplaintService { case "ERS": builder = this._allegationComplaintRepository .createQueryBuilder("allegation") - .addSelect( - "GREATEST(complaint.update_utc_timestamp, allegation.update_utc_timestamp, COALESCE((SELECT MAX(update.update_utc_timestamp) FROM complaint_update update WHERE update.complaint_identifier = complaint.complaint_identifier), '1970-01-01'))", - "_update_utc_timestamp", - ) .leftJoinAndSelect("allegation.complaint_identifier", "complaint") .leftJoin("allegation.violation_code", "violation_code") .addSelect([ @@ -248,10 +253,6 @@ export class ComplaintService { case "GIR": builder = this._girComplaintRepository .createQueryBuilder("general") - .addSelect( - "GREATEST(complaint.update_utc_timestamp, general.update_utc_timestamp, COALESCE((SELECT MAX(update.update_utc_timestamp) FROM complaint_update update WHERE update.complaint_identifier = complaint.complaint_identifier), '1970-01-01'))", - "_update_utc_timestamp", - ) .leftJoinAndSelect("general.complaint_identifier", "complaint") .leftJoin("general.gir_type_code", "gir") .addSelect(["gir.gir_type_code", "gir.short_description", "gir.long_description"]); @@ -260,10 +261,6 @@ export class ComplaintService { default: builder = this._wildlifeComplaintRepository .createQueryBuilder("wildlife") //-- alias the hwcr_complaint - .addSelect( - "GREATEST(complaint.update_utc_timestamp, wildlife.update_utc_timestamp, COALESCE((SELECT MAX(update.update_utc_timestamp) FROM complaint_update update WHERE update.complaint_identifier = complaint.complaint_identifier), '1970-01-01'))", - "_update_utc_timestamp", - ) .leftJoinAndSelect("wildlife.complaint_identifier", "complaint") .leftJoin("wildlife.species_code", "species_code") .addSelect([ @@ -1007,7 +1004,8 @@ export class ComplaintService { const skip = page && pageSize ? (page - 1) * pageSize : 0; const sortTable = this._getSortTable(sortBy); - const sortString = sortBy !== "update_utc_timestamp" ? `${sortTable}.${sortBy}` : "_update_utc_timestamp"; + const sortString = + sortBy !== "update_utc_timestamp" ? `${sortTable}.${sortBy}` : "complaint.comp_last_upd_utc_timestamp"; //-- generate initial query let builder = this._generateQueryBuilder(complaintType); @@ -1057,7 +1055,7 @@ export class ComplaintService { //-- apply sort if provided if (sortBy && orderBy) { builder - .orderBy(sortString, orderBy) + .orderBy(sortString, orderBy, "NULLS LAST") .addOrderBy( "complaint.incident_reported_utc_timestmp", sortBy === "incident_reported_utc_timestmp" ? orderBy : "DESC", @@ -1065,11 +1063,8 @@ export class ComplaintService { } //-- search and count - // Workaround for the issue with getManyAndCount() returning the wrong count and results in complex queries - // introduced by adding an IN clause in a OrWhere statement: https://github.com/typeorm/typeorm/issues/320 - const [, total] = await builder.take(0).getManyAndCount(); // returns 0 results but the total count is correct + const [complaints, total] = await builder.skip(skip).take(pageSize).getManyAndCount(); results.totalCount = total; - const complaints = page && pageSize ? await builder.skip(skip).take(pageSize).getMany() : await builder.getMany(); switch (complaintType) { case "ERS": { @@ -1127,14 +1122,15 @@ export class ComplaintService { model: ComplaintMapSearchClusteredParameters, hasCEEBRole: boolean, token?: string, + count: boolean = false, ): Promise> => { const { query, ...filters } = model; try { //-- search for complaints - // Only these options require the cos_geo_org_unit_flat_vw view (cos_organization), which is very slow. + // Only these options require the cos_geo_org_unit_flat_mvw view (cos_organization), which is very slow. const includeCosOrganization: boolean = Boolean(query || filters.community || filters.zone || filters.region); - let builder = this._generateMapQueryBuilder(complaintType, includeCosOrganization); + let builder = this._generateMapQueryBuilder(complaintType, includeCosOrganization, count); //-- apply filters if used if (Object.keys(filters).length !== 0) { @@ -1189,13 +1185,14 @@ export class ComplaintService { token?: string, ): Promise => { try { - const builder = await this._generateFilteredMapQueryBuilder(complaintType, model, hasCEEBRole, token); + const builder = await this._generateFilteredMapQueryBuilder(complaintType, model, hasCEEBRole, token, true); //-- filter for locations without coordinates builder.andWhere("ST_X(complaint.location_geometry_point) = 0"); builder.andWhere("ST_Y(complaint.location_geometry_point) = 0"); - return builder.getCount(); + const results = await builder.getRawOne(); + return results.count ? Number(results.count) : 0; } catch (error) { this.logger.error(error); } @@ -1223,7 +1220,7 @@ export class ComplaintService { ); //-- run mapped query - const mappedComplaints = await complaintBuilder.getMany(); + const mappedComplaints = await complaintBuilder.getRawMany(); // convert to supercluster PointFeature array const points: Array> = mappedComplaints.map((item) => { @@ -1231,9 +1228,9 @@ export class ComplaintService { type: "Feature", properties: { cluster: false, - id: item.complaint_identifier.complaint_identifier, + id: item.complaint_identifier, }, - geometry: item.complaint_identifier.location_geometry_point, + geometry: item.location_geometry_point, } as PointFeature; }); @@ -1314,15 +1311,50 @@ export class ComplaintService { } }; + // There is specific business logic around when a complaint is considered to be 'Updated'. + // This business logic doesn't align with the standard update audit date in a couple of areas + // - When a complaint is new it is considered untouched and never updated + // - When case data associated with a complaint is modified the complaint is considered updated + // - When attachments are uploaded to a complaint or case the complaint is considered updated + // - Updates from webEOC are considered Updates, however Actions Taken are not + // As a result this method can be called whenever you need to set the complaint as 'Updated' + updateComplaintLastUpdatedDate = async (id: string): Promise => { + try { + const idir = getIdirFromRequest(this.request); + const timestamp = new Date(); + + const result = await this.complaintsRepository + .createQueryBuilder("complaint") + .update() + .set({ update_user_id: idir, comp_last_upd_utc_timestamp: timestamp }) + .where("complaint_identifier = :id", { id }) + .execute(); + + //-- check to make sure that only one record was updated + if (result.affected === 1) { + return true; + } else { + this.logger.error(`Unable to update complaint: ${id}`); + throw new HttpException(`Unable to update complaint: ${id}`, HttpStatus.UNPROCESSABLE_ENTITY); + } + } catch (error) { + this.logger.error(`An Error occured trying to update complaint: ${id}`); + this.logger.error(error.response); + + throw new HttpException(`Unable to update complaint: ${id}}`, HttpStatus.BAD_REQUEST); + } + }; + updateComplaintStatusById = async (id: string, status: string): Promise => { try { const idir = getIdirFromRequest(this.request); + const timestamp = new Date(); const statusCode = await this._codeTableService.getComplaintStatusCodeByStatus(status); const result = await this.complaintsRepository .createQueryBuilder("complaint") .update() - .set({ complaint_status_code: statusCode, update_user_id: idir }) + .set({ complaint_status_code: statusCode, update_user_id: idir, comp_last_upd_utc_timestamp: timestamp }) .where("complaint_identifier = :id", { id }) .execute(); @@ -1419,8 +1451,9 @@ export class ComplaintService { complaintTable.comp_mthd_recv_cd_agcy_cd_xref = xref; - //set the audit field + //set the audit fields complaintTable.update_user_id = idir; + complaintTable.comp_last_upd_utc_timestamp = new Date(); const complaintUpdateResult = await this.complaintsRepository .createQueryBuilder("complaint") @@ -1591,6 +1624,7 @@ export class ComplaintService { entity.update_user_id = idir; entity.complaint_identifier = complaintId; entity.owned_by_agency_code = agencyCode; + entity.comp_last_upd_utc_timestamp = null; // do not want to set this value on a create const xref = await this._compMthdRecvCdAgcyCdXrefService.findByComplaintMethodReceivedCodeAndAgencyCode( model.complaintMethodReceivedCode, @@ -1772,6 +1806,10 @@ export class ComplaintService { }; const _applyTimezone = (input: Date, tz: string, output: "date" | "time" | "datetime"): string => { + if (!input) { + return "N/A"; // No date, so just return a placeholder string for the report + } + const utcDate = toDate(input, { timeZone: "UTC" }); const zonedDate = toZonedTime(utcDate, tz); diff --git a/backend/src/v1/complaint/entities/complaint.entity.ts b/backend/src/v1/complaint/entities/complaint.entity.ts index d7f713d62..5343da2f1 100644 --- a/backend/src/v1/complaint/entities/complaint.entity.ts +++ b/backend/src/v1/complaint/entities/complaint.entity.ts @@ -237,6 +237,14 @@ export class Complaint { @OneToMany(() => ActionTaken, (action_taken) => action_taken.complaintIdentifier) action_taken: ActionTaken[]; + @ApiProperty({ + example: "2003-04-12 04:05:06", + description: + "The time the complaint was last updated, or null if the complaint has never been touched. This value might also be updated by business logic that touches sub-tables to indicate that the business object complaint has been updated.", + }) + @Column({ nullable: true }) + comp_last_upd_utc_timestamp: Date; + constructor( detail_text?: string, caller_name?: string, @@ -269,6 +277,7 @@ export class Complaint { is_privacy_requested?: string, complaint_update?: ComplaintUpdate[], action_taken?: ActionTaken[], + comp_last_upd_utc_timestamp?: Date, ) { this.detail_text = detail_text; this.caller_name = caller_name; @@ -301,5 +310,6 @@ export class Complaint { this.is_privacy_requested = is_privacy_requested; this.complaint_update = complaint_update; this.action_taken = action_taken; + this.comp_last_upd_utc_timestamp = comp_last_upd_utc_timestamp; } } diff --git a/backend/src/v1/cos_geo_org_unit/entities/cos_geo_org_unit.entity.ts b/backend/src/v1/cos_geo_org_unit/entities/cos_geo_org_unit.entity.ts index 360121feb..498387d1d 100644 --- a/backend/src/v1/cos_geo_org_unit/entities/cos_geo_org_unit.entity.ts +++ b/backend/src/v1/cos_geo_org_unit/entities/cos_geo_org_unit.entity.ts @@ -1,7 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { ViewEntity, Column, PrimaryColumn } from "typeorm"; -@ViewEntity("cos_geo_org_unit_flat_vw") +@ViewEntity("cos_geo_org_unit_flat_mvw") export class CosGeoOrgUnit { @ApiProperty({ example: "KTNY", description: "Human readable region code" }) @Column("character varying", { name: "region_code", length: 10 }) diff --git a/backend/src/v1/document/document.controller.spec.ts b/backend/src/v1/document/document.controller.spec.ts index 07acea7ce..67257d580 100644 --- a/backend/src/v1/document/document.controller.spec.ts +++ b/backend/src/v1/document/document.controller.spec.ts @@ -78,6 +78,7 @@ import { Team } from "../team/entities/team.entity"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; import { TeamService } from "../team/team.service"; import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { CacheModule } from "@nestjs/cache-manager"; describe("DocumentController", () => { let controller: DocumentController; @@ -85,6 +86,7 @@ describe("DocumentController", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [DocumentController], + imports: [CacheModule.register()], providers: [ AutomapperModule, { diff --git a/backend/src/v1/document/document.service.spec.ts b/backend/src/v1/document/document.service.spec.ts index 75fc30e07..f1bde9925 100644 --- a/backend/src/v1/document/document.service.spec.ts +++ b/backend/src/v1/document/document.service.spec.ts @@ -77,13 +77,14 @@ import { Team } from "../team/entities/team.entity"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; import { TeamService } from "../team/team.service"; import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { CacheModule } from "@nestjs/cache-manager"; describe("DocumentService", () => { let service: DocumentService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule], + imports: [AutomapperModule, CacheModule.register()], providers: [ AutomapperModule, { diff --git a/backend/src/v1/officer/officer.controller.spec.ts b/backend/src/v1/officer/officer.controller.spec.ts index 7e50f58af..d971df233 100644 --- a/backend/src/v1/officer/officer.controller.spec.ts +++ b/backend/src/v1/officer/officer.controller.spec.ts @@ -16,6 +16,7 @@ import { Team } from "../team/entities/team.entity"; import { TeamService } from "../team/team.service"; import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { CacheModule } from "@nestjs/cache-manager"; describe("OfficerController", () => { let controller: OfficerController; @@ -23,6 +24,7 @@ describe("OfficerController", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [OfficerController], + imports: [CacheModule.register()], providers: [ OfficerService, { diff --git a/backend/src/v1/officer/officer.service.spec.ts b/backend/src/v1/officer/officer.service.spec.ts index 31ef3346b..56afbfba5 100644 --- a/backend/src/v1/officer/officer.service.spec.ts +++ b/backend/src/v1/officer/officer.service.spec.ts @@ -15,12 +15,14 @@ import { Team } from "../team/entities/team.entity"; import { TeamService } from "../team/team.service"; import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { CacheModule } from "@nestjs/cache-manager"; describe("OfficerService", () => { let service: OfficerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [CacheModule.register()], providers: [ OfficerService, { diff --git a/backend/src/v1/officer/officer.service.v2.spec.ts b/backend/src/v1/officer/officer.service.v2.spec.ts index 38434614b..8ce44f6a8 100644 --- a/backend/src/v1/officer/officer.service.v2.spec.ts +++ b/backend/src/v1/officer/officer.service.v2.spec.ts @@ -22,12 +22,14 @@ import { Team } from "../team/entities/team.entity"; import { TeamService } from "../team/team.service"; import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { CacheModule } from "@nestjs/cache-manager"; describe("Testing: OfficerService", () => { let service: OfficerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [CacheModule.register()], providers: [ OfficerService, { diff --git a/backend/src/v1/person/person.service.ts b/backend/src/v1/person/person.service.ts index 87e8ee35a..abfecfbbb 100644 --- a/backend/src/v1/person/person.service.ts +++ b/backend/src/v1/person/person.service.ts @@ -61,8 +61,8 @@ export class PersonService { .createQueryBuilder("person") .leftJoinAndSelect("person.officer", "officer") .leftJoinAndSelect("officer.office_guid", "office") - .leftJoinAndSelect("office.cos_geo_org_unit", "cos_geo_org_unit_flat_vw") - .where("cos_geo_org_unit_flat_vw.zone_code = :zone_code", { zone_code }); + .leftJoinAndSelect("office.cos_geo_org_unit", "cos_geo_org_unit_flat_mvw") + .where("cos_geo_org_unit_flat_mvw.zone_code = :zone_code", { zone_code }); return queryBuilder.getMany(); } @@ -71,7 +71,7 @@ export class PersonService { .createQueryBuilder("person") .leftJoinAndSelect("person.officer", "officer") .leftJoinAndSelect("officer.office_guid", "office") - .leftJoinAndSelect("office.cos_geo_org_unit", "cos_geo_org_unit_flat_vw") + .leftJoinAndSelect("office.cos_geo_org_unit", "cos_geo_org_unit_flat_mvw") .where("office.office_guid = :office_guid", { office_guid }); return queryBuilder.getMany(); } diff --git a/backend/src/v1/person_complaint_xref/person_complaint_xref.controller.spec.ts b/backend/src/v1/person_complaint_xref/person_complaint_xref.controller.spec.ts index 945ee1efb..839b9968a 100644 --- a/backend/src/v1/person_complaint_xref/person_complaint_xref.controller.spec.ts +++ b/backend/src/v1/person_complaint_xref/person_complaint_xref.controller.spec.ts @@ -5,6 +5,7 @@ import { PersonComplaintXref } from "./entities/person_complaint_xref.entity"; import { getRepositoryToken } from "@nestjs/typeorm"; import { DataSource } from "typeorm"; import { dataSourceMockFactory } from "../../../test/mocks/datasource"; +import { ComplaintService } from "../complaint/complaint.service"; describe("PersonComplaintXrefController", () => { let controller: PersonComplaintXrefController; @@ -14,10 +15,15 @@ describe("PersonComplaintXrefController", () => { controllers: [PersonComplaintXrefController], providers: [ PersonComplaintXrefService, + ComplaintService, { provide: getRepositoryToken(PersonComplaintXref), useValue: {}, }, + { + provide: ComplaintService, + useFactory: dataSourceMockFactory, + }, { provide: DataSource, useFactory: dataSourceMockFactory, diff --git a/backend/src/v1/person_complaint_xref/person_complaint_xref.module.ts b/backend/src/v1/person_complaint_xref/person_complaint_xref.module.ts index e1a9106d0..1ec04f305 100644 --- a/backend/src/v1/person_complaint_xref/person_complaint_xref.module.ts +++ b/backend/src/v1/person_complaint_xref/person_complaint_xref.module.ts @@ -1,11 +1,12 @@ -import { Module } from "@nestjs/common"; +import { forwardRef, Module } from "@nestjs/common"; import { PersonComplaintXrefService } from "./person_complaint_xref.service"; import { PersonComplaintXrefController } from "./person_complaint_xref.controller"; import { TypeOrmModule } from "@nestjs/typeorm"; import { PersonComplaintXref } from "./entities/person_complaint_xref.entity"; +import { ComplaintModule } from "../complaint/complaint.module"; @Module({ - imports: [TypeOrmModule.forFeature([PersonComplaintXref])], + imports: [TypeOrmModule.forFeature([PersonComplaintXref]), forwardRef(() => ComplaintModule)], controllers: [PersonComplaintXrefController], providers: [PersonComplaintXrefService], exports: [PersonComplaintXrefService], diff --git a/backend/src/v1/person_complaint_xref/person_complaint_xref.service.spec.ts b/backend/src/v1/person_complaint_xref/person_complaint_xref.service.spec.ts index 9fb4f40fe..22a5573fa 100644 --- a/backend/src/v1/person_complaint_xref/person_complaint_xref.service.spec.ts +++ b/backend/src/v1/person_complaint_xref/person_complaint_xref.service.spec.ts @@ -4,6 +4,7 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import { DataSource } from "typeorm"; import { dataSourceMockFactory } from "../../../test/mocks/datasource"; import { PersonComplaintXref } from "./entities/person_complaint_xref.entity"; +import { ComplaintService } from "../complaint/complaint.service"; describe("PersonComplaintXrefService", () => { let service: PersonComplaintXrefService; @@ -12,10 +13,15 @@ describe("PersonComplaintXrefService", () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PersonComplaintXrefService, + ComplaintService, { provide: getRepositoryToken(PersonComplaintXref), useValue: {}, }, + { + provide: ComplaintService, + useFactory: dataSourceMockFactory, + }, { provide: DataSource, useFactory: dataSourceMockFactory, diff --git a/backend/src/v1/person_complaint_xref/person_complaint_xref.service.ts b/backend/src/v1/person_complaint_xref/person_complaint_xref.service.ts index 8db568b6c..d14cef59b 100644 --- a/backend/src/v1/person_complaint_xref/person_complaint_xref.service.ts +++ b/backend/src/v1/person_complaint_xref/person_complaint_xref.service.ts @@ -1,17 +1,21 @@ -import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { BadRequestException, forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { CreatePersonComplaintXrefDto } from "./dto/create-person_complaint_xref.dto"; import { PersonComplaintXref } from "./entities/person_complaint_xref.entity"; import { InjectRepository } from "@nestjs/typeorm"; import { DataSource, QueryRunner, Repository } from "typeorm"; +import { ComplaintService } from "../complaint/complaint.service"; @Injectable() export class PersonComplaintXrefService { @InjectRepository(PersonComplaintXref) - private personComplaintXrefRepository: Repository; + private readonly personComplaintXrefRepository: Repository; private readonly logger = new Logger(PersonComplaintXrefService.name); - constructor(private dataSource: DataSource) {} + constructor( + private readonly dataSource: DataSource, + @Inject(forwardRef(() => ComplaintService)) private readonly _complaintService: ComplaintService, + ) {} async create(createPersonComplaintXrefDto: CreatePersonComplaintXrefDto): Promise { const newPersonComplaintXref = this.personComplaintXrefRepository.create(createPersonComplaintXrefDto); @@ -63,19 +67,32 @@ export class PersonComplaintXrefService { .getOne(); } - async update( - //queryRunner: QueryRunner, - person_complaint_xref_guid: any, - updatePersonComplaintXrefDto, - ): Promise { + async update(person_complaint_xref_guid: any, updatePersonComplaintXrefDto): Promise { const updatedValue = await this.personComplaintXrefRepository.update( person_complaint_xref_guid, updatePersonComplaintXrefDto, ); - //queryRunner.manager.save(updatedValue); return this.findOne(person_complaint_xref_guid); } + /** + * + * Update the complaint last updated date on the parent record + */ + async updateComplaintLastUpdatedDate( + complaintIdentifier: string, + newPersonComplaintXref: PersonComplaintXref, + queryRunner: QueryRunner, + ): Promise { + if (await this._complaintService.updateComplaintLastUpdatedDate(complaintIdentifier)) { + // save the transaction + await queryRunner.manager.save(newPersonComplaintXref); + this.logger.debug(`Successfully assigned person to complaint ${complaintIdentifier}`); + } else { + throw new BadRequestException(`Unable to assign person to complaint ${complaintIdentifier}`); + } + } + /** * Assigns an officer to a complaint. This will perform one of two operations. * If the existing complaint is not yet assigned to an officer, then this will create a new complaint/officer cross reference. @@ -110,8 +127,9 @@ export class PersonComplaintXrefService { newPersonComplaintXref = await this.create(createPersonComplaintXrefDto); this.logger.debug(`Updating assignment on complaint ${complaintIdentifier}`); - // save the transaction - await queryRunner.manager.save(newPersonComplaintXref); + // Update the complaint last updated date on the parent record + await this.updateComplaintLastUpdatedDate(complaintIdentifier, newPersonComplaintXref, queryRunner); + await queryRunner.commitTransaction(); this.logger.debug(`Successfully assigned person to complaint ${complaintIdentifier}`); } catch (err) { @@ -156,9 +174,8 @@ export class PersonComplaintXrefService { newPersonComplaintXref = await this.create(createPersonComplaintXrefDto); this.logger.debug(`Updating assignment on complaint ${complaintIdentifier}`); - // save the transaction - await queryRunner.manager.save(newPersonComplaintXref); - this.logger.debug(`Successfully assigned person to complaint ${complaintIdentifier}`); + // Update the complaint last updated date on the parent record + await this.updateComplaintLastUpdatedDate(complaintIdentifier, newPersonComplaintXref, queryRunner); } catch (err) { this.logger.error(err); this.logger.error(`Rolling back assignment on complaint ${complaintIdentifier}`); diff --git a/backend/src/v1/staging_complaint/staging_complaint.service.ts b/backend/src/v1/staging_complaint/staging_complaint.service.ts index 94512355d..2cf9cd5a5 100644 --- a/backend/src/v1/staging_complaint/staging_complaint.service.ts +++ b/backend/src/v1/staging_complaint/staging_complaint.service.ts @@ -107,8 +107,10 @@ export class StagingComplaintService { "back_number_of_days", "back_number_of_hours", "back_number_of_minutes", - "entrydate", "status", + "entrydate", // WebEOC will update the following 3 fields when entering an action taken + "positionname", + "username", ]; // Omit the attributes to ignore diff --git a/backend/src/v1/team/team.controller.spec.ts b/backend/src/v1/team/team.controller.spec.ts index 822de1f37..9586e67b5 100644 --- a/backend/src/v1/team/team.controller.spec.ts +++ b/backend/src/v1/team/team.controller.spec.ts @@ -10,6 +10,7 @@ import { CssService } from "../../external_api/css/css.service"; import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; import { Officer } from "../officer/entities/officer.entity"; +import { CacheModule } from "@nestjs/cache-manager"; describe("TeamController", () => { let controller: TeamController; @@ -17,6 +18,7 @@ describe("TeamController", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [TeamController], + imports: [CacheModule.register()], providers: [ TeamService, { diff --git a/backend/src/v1/team/team.service.spec.ts b/backend/src/v1/team/team.service.spec.ts index 41f59632a..3daa7f972 100644 --- a/backend/src/v1/team/team.service.spec.ts +++ b/backend/src/v1/team/team.service.spec.ts @@ -9,12 +9,14 @@ import { CssService } from "../../external_api/css/css.service"; import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; import { Officer } from "..//officer/entities/officer.entity"; +import { CacheModule } from "@nestjs/cache-manager"; describe("TeamService", () => { let service: TeamService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [CacheModule.register()], providers: [ TeamService, { diff --git a/backend/test/mocks/mock-allegation-complaint-repository.ts b/backend/test/mocks/mock-allegation-complaint-repository.ts index b05d04c45..51a6bbee3 100644 --- a/backend/test/mocks/mock-allegation-complaint-repository.ts +++ b/backend/test/mocks/mock-allegation-complaint-repository.ts @@ -468,6 +468,8 @@ const singleItem = { allegation_complaint_guid: "686ea89d-693b-4fdf-9266-226171e6dbd3", }; +const count = { count: "55" }; + export const MockAllegationComplaintRepository = () => ({ find: jest.fn().mockResolvedValue(manyItems), findOneBy: jest.fn().mockResolvedValue(singleItem), @@ -493,7 +495,11 @@ export const MockAllegationComplaintRepository = () => ({ orWhere: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(manyItems), + getRawMany: jest.fn().mockResolvedValue(manyItems), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), getOne: jest.fn().mockResolvedValue(singleItem), + getRawOne: jest.fn().mockResolvedValue(count), getQuery: jest.fn(), select: jest.fn().mockReturnThis(), addSelect: jest.fn().mockReturnThis(), diff --git a/backend/test/mocks/mock-code-table-repositories.ts b/backend/test/mocks/mock-code-table-repositories.ts index d44d14a63..820217972 100644 --- a/backend/test/mocks/mock-code-table-repositories.ts +++ b/backend/test/mocks/mock-code-table-repositories.ts @@ -195,13 +195,6 @@ const natureOfComplaints = [ display_order: 3, active_ind: true, }, - { - hwcr_complaint_nature_code: "COUGARN", - short_description: "COUGARN", - long_description: "Cougar suspected - killed/injured livestock/pets - not present", - display_order: 4, - active_ind: true, - }, { hwcr_complaint_nature_code: "DAMNP", short_description: "DAMNP", @@ -788,8 +781,10 @@ export const MockCosOrganizationUnitCodeTableRepository = () => ({ map: jest.fn().mockReturnThis(), createQueryBuilder: jest.fn(() => ({ select: jest.fn().mockReturnThis(), - distinctOn: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), getRawMany: jest.fn().mockReturnThis(), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), leftJoinAndSelect: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), @@ -811,9 +806,11 @@ export const MockRegionCodeTableServiceRepository = () => ({ map: jest.fn().mockReturnThis(), createQueryBuilder: jest.fn(() => ({ select: jest.fn().mockReturnThis(), - distinctOn: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), distinct: jest.fn().mockReturnThis(), getRawMany: jest.fn().mockResolvedValue(regions), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockResolvedValue(regions), leftJoinAndSelect: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), getMany: jest.fn().mockReturnThis(), @@ -826,9 +823,11 @@ export const MockZoneCodeTableServiceRepository = () => ({ map: jest.fn().mockReturnThis(), createQueryBuilder: jest.fn(() => ({ select: jest.fn().mockReturnThis(), - distinctOn: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), distinct: jest.fn().mockReturnThis(), getRawMany: jest.fn().mockResolvedValue(zones), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockResolvedValue(zones), leftJoinAndSelect: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), getMany: jest.fn().mockReturnThis(), @@ -841,9 +840,11 @@ export const MockCommunityCodeTableServiceRepository = () => ({ map: jest.fn().mockReturnThis(), createQueryBuilder: jest.fn(() => ({ select: jest.fn().mockReturnThis(), - distinctOn: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), distinct: jest.fn().mockReturnThis(), getRawMany: jest.fn().mockResolvedValue(communities), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockResolvedValue(communities), leftJoinAndSelect: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), getMany: jest.fn().mockReturnThis(), diff --git a/backend/test/mocks/mock-complaints-repositories.ts b/backend/test/mocks/mock-complaints-repositories.ts index 609ccba15..025e7160a 100644 --- a/backend/test/mocks/mock-complaints-repositories.ts +++ b/backend/test/mocks/mock-complaints-repositories.ts @@ -697,6 +697,8 @@ const officers = [ }, ]; +const count = { count: "55" }; + export const MockComplaintsAgencyRepository = () => ({ getIdirFromRequest: jest.fn().mockReturnThis(), find: jest.fn().mockReturnThis(), @@ -732,8 +734,12 @@ export const MockComplaintsRepository = () => ({ set: jest.fn().mockReturnThis(), execute: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(complaints), + getRawMany: jest.fn().mockResolvedValue(complaints), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockResolvedValue(complaints), getCount: jest.fn().mockResolvedValue(complaints.length), getOne: jest.fn().mockResolvedValue(complaints[3]), + getRawOne: jest.fn().mockResolvedValue(count), update: jest.fn().mockResolvedValue({ affected: 1 }), })), }); @@ -752,8 +758,12 @@ export const MockComplaintsRepositoryV2 = () => ({ set: jest.fn().mockReturnThis(), execute: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(complaints), + getRawMany: jest.fn().mockResolvedValue(complaints), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockResolvedValue(complaints), getCount: jest.fn().mockResolvedValue(complaints.length), getOne: jest.fn().mockResolvedValue(complaints[3]), + getRawOne: jest.fn().mockResolvedValue(count), update: jest.fn().mockResolvedValue({ affected: 1 }), }; }), diff --git a/backend/test/mocks/mock-general-incident-complaint-repository.ts b/backend/test/mocks/mock-general-incident-complaint-repository.ts index 3a964a232..f6e67cfb9 100644 --- a/backend/test/mocks/mock-general-incident-complaint-repository.ts +++ b/backend/test/mocks/mock-general-incident-complaint-repository.ts @@ -468,6 +468,8 @@ const singleItem = { allegation_complaint_guid: "686ea89d-693b-4fdf-9266-226171e6dbd3", }; +const count = { count: "55" }; + export const MockGeneralIncidentComplaintRepository = () => ({ find: jest.fn().mockResolvedValue(manyItems), findOneBy: jest.fn().mockResolvedValue(singleItem), @@ -493,7 +495,11 @@ export const MockGeneralIncidentComplaintRepository = () => ({ orWhere: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(manyItems), + getRawMany: jest.fn().mockResolvedValue(manyItems), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), getOne: jest.fn().mockResolvedValue(singleItem), + getRawOne: jest.fn().mockResolvedValue(count), getQuery: jest.fn(), select: jest.fn().mockReturnThis(), addSelect: jest.fn().mockReturnThis(), diff --git a/backend/test/mocks/mock-wildlife-conflict-complaint-repository.ts b/backend/test/mocks/mock-wildlife-conflict-complaint-repository.ts index 72a2fc3b0..0beffefa3 100644 --- a/backend/test/mocks/mock-wildlife-conflict-complaint-repository.ts +++ b/backend/test/mocks/mock-wildlife-conflict-complaint-repository.ts @@ -344,6 +344,8 @@ const singleItem = { other_attractants_text: null, }; +const count = { count: "55" }; + export const MockWildlifeConflictComplaintRepository = () => ({ find: jest.fn().mockResolvedValue(manyItems), findOneBy: jest.fn().mockResolvedValue(singleItem), @@ -355,7 +357,6 @@ export const MockWildlifeConflictComplaintRepository = () => ({ return Promise.resolve(true); }), update: jest.fn(() => { - console.log("DERP"); return Promise.resolve(true); }), delete: jest.fn(() => { @@ -373,7 +374,11 @@ export const MockWildlifeConflictComplaintRepository = () => ({ orWhere: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(manyItems), + getRawMany: jest.fn().mockResolvedValue(manyItems), + distinctOn: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), getOne: jest.fn().mockResolvedValue(singleItem), + getRawOne: jest.fn().mockResolvedValue(count), getQuery: jest.fn(), select: jest.fn().mockReturnThis(), addSelect: jest.fn().mockReturnThis(), diff --git a/exports/ceeb_complaint_export.sql b/exports/ceeb_complaint_export.sql index 907b11100..5eae99e20 100644 --- a/exports/ceeb_complaint_export.sql +++ b/exports/ceeb_complaint_export.sql @@ -37,7 +37,7 @@ join 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 + cos_geo_org_unit_flat_mvw 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 diff --git a/exports/cos_hwcr_complaint_export.sql b/exports/cos_hwcr_complaint_export.sql index 2f45f40ca..4877510be 100644 --- a/exports/cos_hwcr_complaint_export.sql +++ b/exports/cos_hwcr_complaint_export.sql @@ -32,7 +32,7 @@ join 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 + cos_geo_org_unit_flat_mvw gfv on gfv.area_code = goc.geo_organization_unit_code left join person_complaint_xref pcx on pcx.complaint_identifier = cmp.complaint_identifier and pcx.active_ind = true left join diff --git a/frontend/cypress/e2e/allegation-details-edit.cy.ts b/frontend/cypress/e2e/allegation-details-edit.cy.ts index 91b9c7c80..0197c1339 100644 --- a/frontend/cypress/e2e/allegation-details-edit.cy.ts +++ b/frontend/cypress/e2e/allegation-details-edit.cy.ts @@ -354,46 +354,46 @@ describe("Complaint Edit Page spec - Edit Allegation View", () => { // Species - not on ERS tab cy.get("#species-pair-id").should("not.exist"); - // Violation Type + // Violation type cy.get("#violation-type-pair-id label").should(($label) => { - expect($label).to.contain.text("Violation Type"); + expect($label).to.contain.text("Violation type"); }); cy.get("#violation-type-pair-id .comp-details-input").should("exist"); - // Officer Assigned + // Officer assigned cy.get("#officer-assigned-pair-id label").should(($label) => { - expect($label).to.contain.text("Officer Assigned"); + expect($label).to.contain.text("Officer assigned"); }); cy.get("#officer-assigned-pair-id .comp-details-input").contains("None"); // Check the Call Details inputs // Complaint Location cy.get("#complaint-location-pair-id label").should(($label) => { - expect($label).to.contain.text("Complaint Location"); + expect($label).to.contain.text("Complaint location"); }); cy.get("#complaint-location-pair-id input").should("exist"); // Incident Time cy.get("#incident-time-pair-id label").should(($label) => { - expect($label).to.contain.text("Incident Date/Time"); + expect($label).to.contain.text("Incident date/time"); }); cy.get("#incident-time-pair-id input").should("exist"); // Location Description cy.get("#location-description-pair-id label").should(($label) => { - expect($label).to.contain.text("Location Description"); + expect($label).to.contain.text("Location description"); }); cy.get("#location-description-pair-id textarea").should("exist"); // Violation In Progress cy.get("#violation-in-progress-pair-id label").should(($label) => { - expect($label).to.contain.text("Violation in Progress"); + expect($label).to.contain.text("Violation in progress"); }); cy.get("#violation-in-progress-pair-id div").should("exist"); // Violation observed cy.get("#violation-observed-pair-id label").should(($label) => { - expect($label).to.contain.text("Violation Observed"); + expect($label).to.contain.text("Violation observed"); }); cy.get("#violation-observed-pair-id div").should("exist"); @@ -457,19 +457,19 @@ describe("Complaint Edit Page spec - Edit Allegation View", () => { // Primary Phone cy.get("#primary-phone-pair-id label").should(($label) => { - expect($label).to.contain.text("Primary Phone"); + expect($label).to.contain.text("Primary phone"); }); cy.get("#primary-phone-pair-id input").should("exist"); // Alternative 1 Phone cy.get("#secondary-phone-pair-id label").should(($label) => { - expect($label).to.contain.text("Alternate Phone 1"); + expect($label).to.contain.text("Alternate phone 1"); }); cy.get("#secondary-phone-pair-id input").should("exist"); // Alternative 2 Phone cy.get("#alternate-phone-pair-id label").should(($label) => { - expect($label).to.contain.text("Alternate Phone 2"); + expect($label).to.contain.text("Alternate phone 2"); }); cy.get("#alternate-phone-pair-id input").should("exist"); @@ -487,7 +487,7 @@ describe("Complaint Edit Page spec - Edit Allegation View", () => { // Reffered by / Complaint Agency cy.get("#reported-pair-id label").should(($label) => { - expect($label).to.contain.text("Organization Reporting the Complaint"); + expect($label).to.contain.text("Organization reporting the complaint"); }); cy.get("#reported-pair-id input").should("exist"); diff --git a/frontend/cypress/e2e/change-complaint-status.cy.ts b/frontend/cypress/e2e/change-complaint-status.cy.ts index e0a163fe2..05cd90905 100644 --- a/frontend/cypress/e2e/change-complaint-status.cy.ts +++ b/frontend/cypress/e2e/change-complaint-status.cy.ts @@ -125,7 +125,7 @@ const canChangeStatus = (reviewComplete: boolean) => { cy.get("#update-status-icon").filter(":visible").click({ force: true }); - cy.get(".change_status_modal") + cy.get(".status-change-subtext") .should("contain", reviewComplete ? "" : "Complaint is pending review.") .find("#complaint_status_dropdown input") .should(reviewComplete ? "be.enabled" : "be.disabled"); diff --git a/frontend/cypress/e2e/complaints-on-map-view.cy.ts b/frontend/cypress/e2e/complaints-on-map-view.cy.ts index 69d3d997a..3d440eee8 100644 --- a/frontend/cypress/e2e/complaints-on-map-view.cy.ts +++ b/frontend/cypress/e2e/complaints-on-map-view.cy.ts @@ -112,9 +112,8 @@ describe("Complaints on map tests", () => { cy.get("div.leaflet-container").should("exist"); cy.get(".leaflet-popup").should("not.exist"); - cy.wait(1000); - cy.get(".leaflet-marker-icon").each(($marker, index) => { + cy.get(".map-marker").each(($marker, index) => { // Click the first marker (index 0) if (index === 0) { cy.wrap($marker).should("exist").click({ force: true }); @@ -122,6 +121,7 @@ describe("Complaints on map tests", () => { }); // wait for the popup to load + cy.wait(1000); cy.get(".leaflet-popup").should("exist"); diff --git a/frontend/cypress/e2e/hwcr-details-edit.cy.ts b/frontend/cypress/e2e/hwcr-details-edit.cy.ts index db4cc9410..5389a2f97 100644 --- a/frontend/cypress/e2e/hwcr-details-edit.cy.ts +++ b/frontend/cypress/e2e/hwcr-details-edit.cy.ts @@ -309,9 +309,9 @@ describe("Complaint Edit Page spec - Edit View", () => { // Note: if the layout of this page changes, these selectors that use classes may break // Check the First Section inputs - // Nature of Complaint + // Nature of complaint cy.get("#nature-of-complaint-pair-id label").should(($label) => { - expect($label).to.contain.text("Nature of Complaint"); + expect($label).to.contain.text("Nature of complaint"); }); cy.get("#nature-of-complaint-pair-id .comp-details-input").should("exist"); @@ -321,28 +321,28 @@ describe("Complaint Edit Page spec - Edit View", () => { }); cy.get("#species-pair-id .comp-details-input").should("exist"); - // Officer Assigned + // Officer assigned cy.get("#officer-assigned-pair-id label").should(($label) => { - expect($label).to.contain.text("Officer Assigned"); + expect($label).to.contain.text("Officer assigned"); }); cy.get("#officer-assigned-pair-id .comp-details-input").contains("None"); // Check the Call Details inputs - // Complaint Location + // Complaint location cy.get("#complaint-location-pair-id label").should(($label) => { - expect($label).to.contain.text("Complaint Location"); + expect($label).to.contain.text("Complaint location"); }); cy.get("#complaint-location-pair-id input").should("exist"); // Incident Time cy.get("#incident-time-pair-id label").should(($label) => { - expect($label).to.contain.text("Incident Date/Time"); + expect($label).to.contain.text("Incident date/time"); }); cy.get("#incident-time-pair-id input").should("exist"); - // Location Description + // Location description cy.get("#location-description-pair-id label").should(($label) => { - expect($label).to.contain.text("Location Description"); + expect($label).to.contain.text("Location description"); }); cy.get("#location-description-pair-id textarea").should("exist"); @@ -409,19 +409,19 @@ describe("Complaint Edit Page spec - Edit View", () => { // Primary Phone cy.get("#primary-phone-pair-id label").should(($label) => { - expect($label).to.contain.text("Primary Phone"); + expect($label).to.contain.text("Primary phone"); }); cy.get("#primary-phone-pair-id input").should("exist"); // Alternative 1 Phone cy.get("#secondary-phone-pair-id label").should(($label) => { - expect($label).to.contain.text("Alternate Phone 1"); + expect($label).to.contain.text("Alternate phone 1"); }); cy.get("#secondary-phone-pair-id input").should("exist"); // Alternative 2 Phone cy.get("#alternate-phone-pair-id label").should(($label) => { - expect($label).to.contain.text("Alternate Phone 2"); + expect($label).to.contain.text("Alternate phone 2"); }); cy.get("#alternate-phone-pair-id input").should("exist"); @@ -439,7 +439,7 @@ describe("Complaint Edit Page spec - Edit View", () => { // Reffered by / Complaint Agency cy.get("#reported-pair-id label").should(($label) => { - expect($label).to.contain.text("Organization Reporting the Complaint"); + expect($label).to.contain.text("Organization reporting the complaint"); }); cy.get("#reported-pair-id input").should("exist"); }); diff --git a/frontend/cypress/e2e/hwcr-outcome-equipment.cy.ts b/frontend/cypress/e2e/hwcr-outcome-equipment.cy.ts index 48091195a..9077dd039 100644 --- a/frontend/cypress/e2e/hwcr-outcome-equipment.cy.ts +++ b/frontend/cypress/e2e/hwcr-outcome-equipment.cy.ts @@ -60,7 +60,7 @@ describe("HWCR Outcome Equipment", () => { officer: "TestAcct, ENV", date: "01", toastText: "Equipment has been updated", - equipmentType: "Neck snare", + equipmentType: "Snare", }; cy.get("#equipment-copy-address-button").click(); cy.get("#copy-coordinates-button").click(); diff --git a/frontend/cypress/e2e/hwcr-outcome-review.cy.ts b/frontend/cypress/e2e/hwcr-outcome-review.cy.ts index 278894282..ccc47e5d7 100644 --- a/frontend/cypress/e2e/hwcr-outcome-review.cy.ts +++ b/frontend/cypress/e2e/hwcr-outcome-review.cy.ts @@ -123,7 +123,7 @@ describe("HWCR File Review", () => { cy.get("#details-screen-update-status-button").click({ force: true }); - cy.get(".change_status_modal") + cy.get(".status-change-subtext") .should("contain", "Complaint is pending review.") .find("#complaint_status_dropdown input") .should("be.disabled"); diff --git a/frontend/cypress/e2e/zone-at-a-glance-header.cy.ts b/frontend/cypress/e2e/zone-at-a-glance-header.cy.ts index 63258c683..44d0a917b 100644 --- a/frontend/cypress/e2e/zone-at-a-glance-header.cy.ts +++ b/frontend/cypress/e2e/zone-at-a-glance-header.cy.ts @@ -16,7 +16,7 @@ describe("COMPENF-259 Zone at a Glance - View Complaint Stats", () => { cy.get(".comp-loader-overlay", { timeout: 30000 }).should("not.exist"); //-- make sure we're on the zone at a glance page - cy.get(".comp-main-content").contains("Zone At a Glance"); + cy.get(".comp-main-content").contains("Zone at a glance"); //-- navigate back to complaints cy.get("#complaints-link").click({ force: true }); diff --git a/frontend/cypress/e2e/zone-at-a-glance-setup.cy.ts b/frontend/cypress/e2e/zone-at-a-glance-setup.cy.ts index 6f7133184..3d2589fdb 100644 --- a/frontend/cypress/e2e/zone-at-a-glance-setup.cy.ts +++ b/frontend/cypress/e2e/zone-at-a-glance-setup.cy.ts @@ -41,7 +41,7 @@ describe("COMPENF-137 Zone at a Glance - Page Set Up", () => { cy.waitForSpinner(); //-- make sure we're on the zone at a glance page - cy.get(".comp-main-content").contains("Zone At a Glance"); + cy.get(".comp-main-content").contains("Zone at a glance"); //-- navigate back to complaints cy.get("#complaints-link").click(); diff --git a/frontend/src/app/common/api.ts b/frontend/src/app/common/api.ts index 265343357..76ec2f568 100644 --- a/frontend/src/app/common/api.ts +++ b/frontend/src/app/common/api.ts @@ -117,6 +117,9 @@ export const get = ( axios .get(url, config) .then((response: AxiosResponse) => { + if (!response) { + return reject(new Error("No response")); + } const { data, status } = response; if (status === STATUS_CODES.Unauthorized) { diff --git a/frontend/src/app/common/attachment-utils.ts b/frontend/src/app/common/attachment-utils.ts index 778af7085..1c56f72fd 100644 --- a/frontend/src/app/common/attachment-utils.ts +++ b/frontend/src/app/common/attachment-utils.ts @@ -1,5 +1,5 @@ import AttachmentEnum from "@constants/attachment-enum"; -import { deleteAttachments, getAttachments, saveAttachments } from "@store/reducers/attachments"; +import { deleteAttachments, saveAttachments } from "@store/reducers/attachments"; import { COMSObject } from "@apptypes/coms/object"; // used to update the state of attachments that are to be added to a complaint @@ -30,27 +30,36 @@ export const handleDeleteAttachments = ( } }; +interface PersistAttachmentsParams { + dispatch: any; + attachmentsToAdd: File[] | null; + attachmentsToDelete: COMSObject[] | null; + complaintIdentifier: string; + setAttachmentsToAdd: any; + setAttachmentsToDelete: any; + attachmentType: AttachmentEnum; + complaintType: string; +} + // Given a list of attachments to add/delete, call COMS to add/delete those attachments -export async function handlePersistAttachments( - dispatch: any, - attachmentsToAdd: File[] | null, - attachmentsToDelete: COMSObject[] | null, - complaintIdentifier: string, - setAttachmentsToAdd: any, - setAttachmentsToDelete: any, - attachmentType: AttachmentEnum, -) { +export async function handlePersistAttachments({ + dispatch, + attachmentsToAdd, + attachmentsToDelete, + complaintIdentifier, + setAttachmentsToAdd, + setAttachmentsToDelete, + attachmentType, + complaintType, +}: PersistAttachmentsParams) { if (attachmentsToDelete) { - await dispatch(deleteAttachments(attachmentsToDelete)); + dispatch(deleteAttachments(attachmentsToDelete, complaintIdentifier, complaintType, attachmentType)); } if (attachmentsToAdd) { - await dispatch(saveAttachments(attachmentsToAdd, complaintIdentifier, attachmentType)); + dispatch(saveAttachments(attachmentsToAdd, complaintIdentifier, complaintType, attachmentType)); } - // refresh store - await dispatch(getAttachments(complaintIdentifier, attachmentType)); - // Clear the attachments since they've been added or saved. setAttachmentsToAdd(null); setAttachmentsToDelete(null); diff --git a/frontend/src/app/common/methods.tsx b/frontend/src/app/common/methods.tsx index 70addce5f..1b24ba9d4 100644 --- a/frontend/src/app/common/methods.tsx +++ b/frontend/src/app/common/methods.tsx @@ -273,8 +273,12 @@ export const parseCoordinates = (coordinates: Coordinate, coordinateType: Coordi // Helper function for determining what type of complaint your are working with // so you can pass that type onto the backend for proper processing export const getComplaintType = ( - complaint: WildlifeComplaintDto | AllegationComplaintDto | GeneralIncidentComplaintDto, + complaint: WildlifeComplaintDto | AllegationComplaintDto | GeneralIncidentComplaintDto | null, ): string => { + if (!complaint) { + return "Unknown"; + } + if ("hwcrId" in complaint) { return COMPLAINT_TYPES.HWCR; } diff --git a/frontend/src/app/common/validation-date-picker.tsx b/frontend/src/app/common/validation-date-picker.tsx index c0d1e63b7..bb37b9ecc 100644 --- a/frontend/src/app/common/validation-date-picker.tsx +++ b/frontend/src/app/common/validation-date-picker.tsx @@ -11,6 +11,7 @@ interface ValidationDatePickerProps { id: string; classNamePrefix: string; errMsg: string; + isDisabled?: boolean; } export const ValidationDatePicker: FC = ({ @@ -23,6 +24,7 @@ export const ValidationDatePicker: FC = ({ id, classNamePrefix, errMsg, + isDisabled, }) => { const handleDateChange = (date: Date) => { onChange(date); @@ -48,6 +50,7 @@ export const ValidationDatePicker: FC = ({ autoComplete="false" monthsShown={2} showPreviousMonths + disabled={isDisabled} />
{errMsg}
diff --git a/frontend/src/app/common/validation-textarea.tsx b/frontend/src/app/common/validation-textarea.tsx index 9d6a3df3a..1b6f6e77a 100644 --- a/frontend/src/app/common/validation-textarea.tsx +++ b/frontend/src/app/common/validation-textarea.tsx @@ -9,6 +9,7 @@ interface ValidationTextAreaProps { rows: number; maxLength?: number; placeholderText?: string; + disabled?: boolean; } export const ValidationTextArea: FC = ({ @@ -20,6 +21,7 @@ export const ValidationTextArea: FC = ({ rows, maxLength, placeholderText, + disabled, }) => { const errClass = errMsg === "" ? "" : "error-message"; const calulatedClass = errMsg === "" ? className : className + " error-border"; @@ -34,6 +36,7 @@ export const ValidationTextArea: FC = ({ onChange={(e) => onChange(e.target.value)} maxLength={maxLength} placeholder={placeholderText} + disabled={disabled} />
{errMsg}
diff --git a/frontend/src/app/components/common/attachment-upload.tsx b/frontend/src/app/components/common/attachment-upload.tsx index 504d4e72e..e3e2839ba 100644 --- a/frontend/src/app/components/common/attachment-upload.tsx +++ b/frontend/src/app/components/common/attachment-upload.tsx @@ -3,9 +3,10 @@ import { BsPlus } from "react-icons/bs"; type Props = { onFileSelect: (selectedFile: FileList) => void; + disabled?: boolean | null; }; -export const AttachmentUpload: FC = ({ onFileSelect }) => { +export const AttachmentUpload: FC = ({ onFileSelect, disabled }) => { const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files) { onFileSelect(event.target.files); @@ -40,6 +41,8 @@ export const AttachmentUpload: FC = ({ onFileSelect }) => { className="comp-attachment-upload-btn" tabIndex={0} onClick={handleDivClick} + disabled={disabled ?? false} + style={disabled ?? false ? { cursor: "default" } : {}} >
diff --git a/frontend/src/app/components/common/attachments-carousel.tsx b/frontend/src/app/components/common/attachments-carousel.tsx index 9fe567585..6f9949e73 100644 --- a/frontend/src/app/components/common/attachments-carousel.tsx +++ b/frontend/src/app/components/common/attachments-carousel.tsx @@ -21,6 +21,7 @@ type Props = { onFileDeleted?: (attachments: COMSObject) => void; onSlideCountChange?: (count: number) => void; setCancelPendingUpload?: (isCancelUpload: boolean) => void | null; + disabled?: boolean | null; }; export const AttachmentsCarousel: FC = ({ @@ -33,6 +34,7 @@ export const AttachmentsCarousel: FC = ({ onFileDeleted, onSlideCountChange, setCancelPendingUpload, + disabled, }) => { const dispatch = useAppDispatch(); @@ -207,7 +209,12 @@ export const AttachmentsCarousel: FC = ({ className="comp-carousel" > - {allowUpload && } + {allowUpload && ( + + )} {slides?.map((item, index) => ( void; isDisabled?: boolean; isClearable?: boolean; + showInactive?: boolean; }; export const CompSelect: FC = ({ @@ -32,13 +33,17 @@ export const CompSelect: FC = ({ errorMessage, isDisabled, isClearable, + showInactive = true, }) => { let styles: StylesConfig = {}; let items: Option[] = []; if (options) { - items = [...options]; + items = [...options.filter((o) => (showInactive ? true : o.isActive))]; + if (value && !items.find((o) => o.value === value.value)) { + items.push(value); + } } // If "none" is an option, lighten the colour a bit so that it doesn't appear the same as the other selectable options diff --git a/frontend/src/app/components/common/map-list-toggle.tsx b/frontend/src/app/components/common/map-list-toggle.tsx index 982e48d53..854c35685 100644 --- a/frontend/src/app/components/common/map-list-toggle.tsx +++ b/frontend/src/app/components/common/map-list-toggle.tsx @@ -26,7 +26,7 @@ const MapListToggle: React.FC = ({ activeView, onToggle, className }) => onChange={(view) => onToggle(view as "list" | "map")} > = ({ activeView, onToggle, className }) => List = ({
-

User Administration

+

User administration

diff --git a/frontend/src/app/components/containers/complaints/complaint-filter.tsx b/frontend/src/app/components/containers/complaints/complaint-filter.tsx index c1d3a8c9f..5a0c84a6e 100644 --- a/frontend/src/app/components/containers/complaints/complaint-filter.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-filter.tsx @@ -127,7 +127,7 @@ export const ComplaintFilter: FC = ({ type }) => { activeFilters.showSpeciesFilter && ( // wildlife only filter <>
- +
= ({ type }) => { activeFilters.showViolationFilter && ( // wildlife only filter
{/* */} - +
= ({ type }) => { {COMPLAINT_TYPES.GIR === type && activeFilters.showGirTypeFilter && ( // GIR only filter
- +
= ({ type }) => { {activeFilters.showDateFilter && ( = ({ type }) => { {COMPLAINT_TYPES.ERS === type && activeFilters.showMethodFilter && (
- +
= ({ type }) => { {UserService.hasRole(Roles.CEEB) && (
- +
= ({ type }) => { {COMPLAINT_TYPES.HWCR === type && activeFilters.showOutcomeAnimalFilter && (
- +
= ({ type }) => { {COMPLAINT_TYPES.HWCR === type && activeFilters.showOutcomeAnimalDateFilter && ( = ({ type }) => { )} {activeFilters.showOfficerFilter && (
- +
= ({ defaultComplaintType }) => {

Complaints

- +
{/* */} diff --git a/frontend/src/app/components/containers/complaints/details/call-details.tsx b/frontend/src/app/components/containers/complaints/details/call-details.tsx index 6361e6311..5adb5452e 100644 --- a/frontend/src/app/components/containers/complaints/details/call-details.tsx +++ b/frontend/src/app/components/containers/complaints/details/call-details.tsx @@ -5,7 +5,6 @@ import { formatDate, formatTime } from "@common/methods"; import { ComplaintDetailsAttractant } from "@apptypes/complaints/details/complaint-attactant"; import { selectComplaintDetails } from "@store/reducers/complaints"; import COMPLAINT_TYPES from "@apptypes/app/complaint-types"; -import { ComplaintDetails } from "@apptypes/complaints/details/complaint-details"; import { FEATURE_TYPES } from "@constants/feature-flag-types"; import { FeatureFlag } from "@components/common/feature-flag"; import { CompLocationInfo } from "@components/common/comp-location-info"; @@ -29,24 +28,24 @@ export const CallDetails: FC = ({ complaintType }) => { violationInProgress, violationObserved, complaintMethodReceivedCode, - } = useAppSelector(selectComplaintDetails(complaintType)) as ComplaintDetails; + } = useAppSelector((state) => selectComplaintDetails(state, complaintType)); return (
-

Call Details

+

Call details

{/* General Call Information */}
-
Complaint Description
+
Complaint description
{details}
-
Incident Date/Time
+
Incident date/time
= ({ complaintType }) => { {complaintType === COMPLAINT_TYPES.ERS && ( <>
-
Violation In Progress
+
Violation in progress
{violationInProgress ? "Yes" : "No"}
-
Violation Observed
+
Violation observed
= ({ complaintType }) => { {/* Location Information */}
-
Complaint Location
+
Complaint location
{location}
-
Location Description
+
Location description
{locationDescription}
{ return (
-

Caller Information

+

Caller information

@@ -35,15 +35,15 @@ export const CallerInformation: FC = () => {
{name}
-
Primary Phone
+
Primary phone
{formatPhoneNumber(primaryPhone !== undefined ? primaryPhone : "")}
-
Alternative Phone 1
+
Alternative phone 1
{formatPhoneNumber(secondaryPhone !== undefined ? secondaryPhone : "")}
-
Alternative Phone 2
+
Alternative phone 2
{formatPhoneNumber(alternatePhone !== undefined ? alternatePhone : "")}
@@ -55,7 +55,7 @@ export const CallerInformation: FC = () => {
{email}
-
Organization Reporting the Complaint
+
Organization reporting the complaint
{reportedByCode?.longDescription}
diff --git a/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx b/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx index 071c1d3ea..6488351ab 100644 --- a/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx +++ b/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx @@ -50,7 +50,6 @@ import { ComplaintAlias } from "@apptypes/app/aliases"; import AttachmentEnum from "@constants/attachment-enum"; import { getUserAgency } from "@service/user-service"; import { useSelector } from "react-redux"; -import { ComplaintDetails } from "@apptypes/complaints/details/complaint-details"; import { FEATURE_TYPES } from "@constants/feature-flag-types"; import { FeatureFlag } from "@components/common/feature-flag"; @@ -116,8 +115,8 @@ export const CreateComplaint: FC = () => { const [secondaryPhoneMsg, setSecondaryPhoneMsg] = useState(""); const [alternatePhoneMsg, setAlternatePhoneMsg] = useState(""); const [selectedIncidentDateTime, setSelectedIncidentDateTime] = useState(); - const complaintMethodReceivedCodes = useSelector(selectComplaintReceivedMethodDropdown) as Option[]; - const { complaintMethodReceivedCode } = useAppSelector(selectComplaintDetails(complaintType)) as ComplaintDetails; + const complaintMethodReceivedCodes = useSelector(selectComplaintReceivedMethodDropdown); + const { complaintMethodReceivedCode } = useAppSelector((state) => selectComplaintDetails(state, complaintType)); const selectedComplaintMethodReceivedCode = complaintMethodReceivedCodes.find( (option) => option.value === complaintMethodReceivedCode?.complaintMethodReceivedCode, ); @@ -585,15 +584,16 @@ export const CreateComplaint: FC = () => { const handleComplaintProcessing = async (complaint: ComplaintAlias) => { let complaintId = await handleHwcrComplaint(complaint); if (complaintId) { - handlePersistAttachments( + handlePersistAttachments({ dispatch, attachmentsToAdd, attachmentsToDelete, - complaintId, + complaintIdentifier: complaintId, setAttachmentsToAdd, setAttachmentsToDelete, - AttachmentEnum.COMPLAINT_ATTACHMENT, - ); + attachmentType: AttachmentEnum.COMPLAINT_ATTACHMENT, + complaintType, + }); } setErrorNotificationClass("comp-complaint-error display-none"); @@ -625,7 +625,7 @@ export const CreateComplaint: FC = () => {
-

Complaint Details

+

Complaint details

{/* Error Alert */} @@ -654,7 +654,7 @@ export const CreateComplaint: FC = () => { id="nature-of-complaint-label-id" htmlFor="complaint-type-select-id" > - Complaint Type* + Complaint type*
{ id="officer-assigned-select-label-id" htmlFor="officer-assigned-select-id" > - Officer Assigned + Officer assigned
{
- Call Details + Call details {/* HWCR Species and Nature of Complaint */} {complaintType === COMPLAINT_TYPES.HWCR && ( @@ -730,7 +730,7 @@ export const CreateComplaint: FC = () => { id="nature-of-complaint-label-id" htmlFor="nature-of-complaint-select-id" > - Nature of Complaint* + Nature of complaint*
{ onChange={(e) => handleNatureOfComplaintChange(e)} className="comp-details-input" options={hwcrNatureOfComplaintCodes} + showInactive={false} placeholder="Select" enableValidation={true} errorMessage={natureOfComplaintErrorMsg} @@ -755,7 +756,7 @@ export const CreateComplaint: FC = () => { id="violation-type-pair-id" >
{ className="col-auto" htmlFor="complaint-description-textarea-id" > - Complaint Description* + Complaint description*
{ className="comp-details-form-row" id="incident-time-pair-id" > - +
{ className="comp-details-form-row" id="violation-in-progress-pair-id" > - +
{ id="complaint-location-label-id" htmlFor="location-edit-id" > - Complaint Location + Complaint location
{ className="comp-details-form-row" id="location-description-pair-id" > - +