# Gatekeeper Report Utils

Shared PDF, email, date, attachment, and display helpers for Gatekeeper JourneyApps reports.

## Install

Add the package to a JourneyApps CloudCode task:

```json
{
  "dependencies": {
    "@gatekeeper_technology/report-utils": "1.3.3"
  }
}
```

Create a `.npmrc` file in the CloudCode task with the read-only npm token from 1Password:

```text
//registry.npmjs.org/:_authToken=<read-only-npm-token>
```

Then import the helpers:

```js
const reportUtils = require("@gatekeeper_technology/report-utils");
```

## Common Examples

### Format Dates

```js
const {
  displayDate,
  displayDateTime,
  displayTime,
  displayTimeWithSeconds,
  getOffsetDate,
  getStartOfWeek,
  getEndOfWeek,
  getWeekNumber,
  isSameDay,
  compareDates,
} = require("@gatekeeper_technology/report-utils");

const date = new Date(2026, 6, 10, 8, 30, 0);

displayDate(date);                  // "10 Jul 2026"
displayDate(date, "long");          // "10 July 2026"
displayTime(date);                  // "08:30"
displayTimeWithSeconds(date);       // "08:30:00"
displayDateTime(date);              // "10 Jul 2026, 08:30"

getOffsetDate(date, 7);             // date plus 7 days
getStartOfWeek(date);               // Monday start, ISO mode by default
getEndOfWeek(date);                 // Sunday end, ISO mode by default
getWeekNumber(date);                // calendar week number

isSameDay(date, new Date());        // true/false
compareDates(date, new Date());     // -1, 0, or 1
```

Non-Date inputs generally return `null` for date comparison helpers and `"-"` for display helpers.

### Prepare SendGrid Recipients

```js
const {
  getToEmailsArray,
  setCheckWhitelistEnvironments,
} = require("@gatekeeper_technology/report-utils");

const recipients = getToEmailsArray(
  [" Manager@Example.com ", "auditor@example.com; area@example.com"],
  ["manager@example.com", "ops@example.com"],
  "staging",
  ["manager@example.com", "area@example.com"]
);

// recipients:
// {
//   to: [{ email: "manager@example.com" }, { email: "area@example.com" }],
//   cc: []
// }
```

`getToEmailsArray` trims, lowercases, de-duplicates, splits comma/semicolon separated strings, removes CC addresses that are already in TO, and filters by whitelist in `staging` and `testing`.

To change which environments use the whitelist:

```js
setCheckWhitelistEnvironments(["testing", "staging", "production"]);
```

### Generate HTML And Save A PDF Attachment

```js
const {
  generateHTML,
  postToJourneyBackend,
} = require("@gatekeeper_technology/report-utils");

async function createReport(request, data) {
  const html = generateHTML(data, __dirname, "templates/report.html");

  const saved = await postToJourneyBackend(
    request,
    "report_pdf",
    html,
    `report-${request.id}.pdf`,
    process.env.JOURNEY_PDF_REPORTS_TOKEN
  );

  if (!saved) {
    throw new Error("Report PDF could not be generated or saved");
  }
}
```

`generateHTML` reads and compiles a Handlebars template. `postToJourneyBackend` generates a PDF with `@journeyapps/pdf-reports`, creates a JourneyApps `Attachment`, assigns it to the requested field, and saves the object.

### Use Photos And Signatures In Reports

```js
const {
  getImageContentForPDF,
  isAttachmentsUploaded,
  loadAttachments,
  loadPhotoOrSignature,
} = require("@gatekeeper_technology/report-utils");

async function buildReportData(inspection) {
  try {
    isAttachmentsUploaded(inspection);
  } catch (error) {
    if (error.cause === "attachments-uploading") {
      // Reschedule the CloudCode task using the app's normal retry pattern.
      throw error;
    }
    throw error;
  }

  return loadAttachments({}, inspection);
}

async function addSignature(inspection) {
  return loadPhotoOrSignature(inspection, "customer_signature");
}

async function addImage(inspection) {
  return {
    image_url: await getImageContentForPDF(inspection.photo, "url"),
    image_base64: await getImageContentForPDF(inspection.photo, "base64"),
  };
}
```

Attachment helpers throw with `cause: "attachments-uploading"` when a JourneyApps attachment has not finished uploading. Catch that error and reschedule the task from the calling app.

When `type` is `"base64"`, `getImageContentForPDF` detects the image MIME type from the base64 payload (PNG, JPEG, GIF, WebP, SVG), falling back to the attachment URL extension, then to PNG.

### Display JourneyApps Fields And Sort Data

```js
const {
  formatText,
  getDisplayLabel,
  getDisplayValue,
  sortArrayByField,
} = require("@gatekeeper_technology/report-utils");

const label = getDisplayLabel(store, "store_type");
const value = getDisplayValue(store, "store_type");

formatText(" Store Visit / Audit ");      // "store_visit_audit"
formatText(" Store Visit / Audit ", "higher"); // "STORE_VISIT_AUDIT"

const stores = [
  { name: "Cape Town", score: 88 },
  { name: "Durban", score: 72 },
];

sortArrayByField(stores, "score", -1);    // sorts highest score first
```

`sortArrayByField` uses JavaScript's native `Array.sort`, so it mutates the original array.

## Exported Helpers

### Date And Time

- `displayDate(date, monthLength)`
- `displayDateTime(date, monthLength)`
- `displayTime(date)`
- `displayTimeWithSeconds(date)`
- `getOffsetDate(originalDate, offsetDays)`
- `getStartOfWeek(date, iso)`
- `getEndOfWeek(date, iso)`
- `getWeekNumber(date, iso)`
- `isSameDay(date1, date2)`
- `compareDates(date1, date2)`

### Email

- `getToEmailsArray(toArray, ccArray, env, whitelist)`
- `setCheckWhitelistEnvironments(environments)`
- `storeValidEmail(emailObject, email)`
- `validateEmail(email)`

### Attachments And PDFs

- `getImageContentForPDF(image, type)`
- `loadImageBase64(path)`
- `loadAttachments(target, objectToCheck)`
- `loadPhotoOrSignature(object, fieldName)`
- `isAttachmentsUploaded(objectToCheck)`
- `isAttachmentUploaded(object, fieldName)`
- `generateHTML(data, dirName, fileName)`
- `postToJourneyBackend(object, fieldName, pdfHtml, fileName, token)`

### General

- `getDisplayValue(object, field)`
- `getDisplayLabel(object, field)`
- `formatText(text, type)`
- `sortArrayByField(objectArray, fieldName, sortDirection)`

## Development

Install with the publish npm token from 1Password:

```text
//registry.npmjs.org/:_authToken=<publish-npm-token>
```

Release checklist:

1. Make code changes in `src/*.ts` and export new helpers from `index.ts`.
2. Add or update Jest tests when behavior changes.
3. Update the version in `package.json`.
4. Add a changelog entry below.
5. Run `npm test`.
6. Publish with `npm publish`.
7. Commit and push the release.

## Changelog

### [1.3.3]

#### Fixed

- `getImageContentForPDF` now detects the correct MIME type for base64 data URIs (PNG, JPEG, GIF, WebP, SVG) instead of treating non-SVG attachments as PNG.
- Date helpers now treat Invalid Date values as invalid and return `null` / `"-"` as documented.

#### Changed

- Rewrote the README with install steps, usage examples, and an exported helpers reference.
- Replaced outdated photo attachment tests with coverage for MIME detection, upload checks, and local image loading.
- Aligned general and date tests with current behavior, and added email utility coverage.
- Added Jest + `ts-jest` so TypeScript sources can be tested directly.

### [1.3.2]

#### Changed

- Bumped `@journeyapps/db` from `^8.0.10` to `^8.1.1`.

### [1.3.1]

#### Changed

- Bumped `@gatekeeper_technology/rollbar-utils` to `^1.2.4`.

### [1.3.0]

#### Changed

- Bumped the package version to `1.3.0`.
- Bumped `@gatekeeper_technology/rollbar-utils` to `^1.2.3`.
- Bumped `@journeyapps/db` to `^8.0.10`.
- Removed the unused `@journeyapps/cloudcode` dependency.

### [1.2.3]

#### Changed

- Bumped `@gatekeeper_technology/rollbar-utils` from `1.1.1` to `^1.2.2`.

### [1.2.2]

#### Fixed

- Fixed `postToJourneyBackend` so it throws when the target field is missing, instead of throwing when the field exists.

#### Changed

- Added parameter documentation for `generateHTML`.

### [1.2.1]

#### Removed

- `dist` ignore file in `.npmignore`.

### [1.2.0]

#### Added

- Added `postToJourneyBackend` function
- Added `journeyApps` npm packages as dependencies to assist with the reportUtil package.
- Added `sortArrayByField` function
- Added `generateHTML` function
- Added `formatText` function
- Added `getImageContentForPDF` function

#### Changed

- Migrated to TypeScript
- All date functions should now be able to accept `Day` variables.
- `.gitignore` file now ignores compiled files.
- `isAttachment(s)Uploaded` functions no longer reschedules. It now throws an error with the cause = `attachments-uploading`

### [1.1.10]

#### Removed

- Removed `loadBase64` function in a race against time.

### [1.1.9]

#### Added

- Jest Test cases
- formatText, sortArrayByField and generateHTML functions
- .d.ts files.

### [1.1.8]

#### Changed

- Added more validation on general utils

#### Added

- Unit Tests on General Utils.

#### Removed

- console.error() on date Utils.

### [1.1.7]

#### Changed

- Fixed funky way of throwing errors
- Added a new utils folder with small internal utils

### [1.1.6]

#### Changed

- When retrying a task, we don't need to specify the CloudCode task name as we can access it programmatically

### [1.1.5]

#### Added

- isSameDay: Checks if the given two dates falls on the same day (second date value defaults to now).
- compareDates: Checks if the given two dates falls on the same date and time (return 0), date 1 is before date 2 (returns -1), otherwise returns 1 (second date value defaults to now).

#### Removed

- isToday: Can be used via the isSameDay() function
- isPastDate and isFutureDate: Can be user via the compareDates() function

#### Changed

- Improved Test file (WIP)

### [1.1.4]

#### Added

- areEqualDates
- areEqualDateTimes
- isToday
- getStartOfWeek
- getEndOfWeek
- getWeekNumber
- isFutureDate
- isPastDate

### [1.1.3]

#### Fixed

- A bug where you cannot import the `report-utils` package in the `/mobile` side of an App.

### [1.1.2]

#### Changed

- [TESTING] Moved the `loadImageBase64` function to `index.js` to see if the required("fs") problem is fixed.

### [1.1.1]

#### Changed

- [TESTING] Moved the require("fs") out of the function.

### [1.1.0]

#### Changed

- Refactored the file structure to separate the concerns.
- Replaced `displayPhoto` with `journeyPhotoToBase64`, it now has a more descriptive name
- Replaced  `loadPhotoOrSignature` with  `getPhotoOrSignatureData`, it's now up to the developer to load the data into a data hash.
- Replaced  `loadSignatureBase64` with  `encodeBase64ImagePNG`, renamed function and changed the input parameter to the actual image data.

### [1.00.07]

#### Added

- `index.js`: Added the ability to specify in which environments to enable email whitelisting.

### [1.00.06]

#### Changed

- `index.js`: Emails are first trimmed and toLowerCase'd before checking if it is valid.

### [1.00.05]

#### Changed

- `index.js`: Changed the way `loadAttachments` function receives retry_count value.

### [1.00.04]

#### Fixed

- `index.js`: a Bug where the `loadPhotoOrSignature` function was out of date.

### [1.00.03]

#### Changed

- `index.js`: Emails check now convert all emails to lowercase before checking for duplicates.

### [1.00.02]

#### Added

- `index.js`: Added function `displayTimeWithSeconds`.

#### Changed

- `index.js`: improved javadocs commenting .

### [1.00.01]

#### Changed

- `Readme.md`: explanation improved.
- `index.js`: Added functions `displayDateTime` and `displayTime`.

### [1.00.00]

#### Added

- `package.json`: Added.
- `index.js`: Added and populated with Gatekeeper's handlebarUtil functions.
- `.npmignore`: Added.
- `.gitignore`: Added.
- `README.md`: Added with description, set-up and change-log.
