Skip to content

Commit

Permalink
Merge branch 'main' into gordon/filters-session-storage
Browse files Browse the repository at this point in the history
  • Loading branch information
gordonfarrell authored Jan 13, 2025
2 parents e681e3e + 2055a30 commit 5cb4673
Show file tree
Hide file tree
Showing 22 changed files with 404 additions and 404 deletions.
1 change: 1 addition & 0 deletions .github/workflows/createNewRelease.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ jobs:
MPI_PORT: 5432
MPI_PATIENT_TABLE: patient
MPI_PERSON_TABLE: person
TRIGGER_CODE_REFERENCE_URL: http://localhost:8086
run: |
npm i -g redoc-cli
CONTAINER=${{ matrix.container }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export const saveMetadataToSqlServer = async (
.input("street_address1", sql.VarChar(255), metadata.street_address1)
.input("street_address2", sql.VarChar(255), metadata.street_address2)
.input("state", sql.VarChar(50), metadata.state)
.input("zip_code", sql.VarChar(50), metadata.zip)
.input("zip_code", sql.VarChar(20), metadata.zip)
.input("latitude", sql.Float, metadata.latitude)
.input("longitude", sql.Float, metadata.longitude)
.input(
Expand Down
47 changes: 3 additions & 44 deletions containers/ecr-viewer/src/app/components/EcrTableClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SortButton } from "@/app/components/SortButton";
import { EcrDisplay } from "@/app/services/listEcrDataService";
import { toSentenceCase } from "@/app/services/formatService";
import { usePathname, useSearchParams, useRouter } from "next/navigation";
import { range } from "../view-data/utils/utils";
import { noData, range } from "../view-data/utils/utils";
import classNames from "classnames";
import Link from "next/link";
import { saveToSessionStorage } from "./utils";
Expand Down Expand Up @@ -277,12 +277,6 @@ const getAriaSortValue = (sortDirection: string): AriaSortType | undefined => {
const DataRow = ({ item }: { item: EcrDisplay }) => {
const patient_first_name = toSentenceCase(item.patient_first_name);
const patient_last_name = toSentenceCase(item.patient_last_name);
const createDateObj = new Date(item.date_created);
const createDateDate = formatDate(createDateObj);
const createDateTime = formatTime(createDateObj);
const patientReportDateObj = new Date(item.patient_report_date);
const patientReportDate = formatDate(patientReportDateObj);
const patientReportTime = formatTime(patientReportDateObj);

const searchParams = useSearchParams();

Expand Down Expand Up @@ -314,16 +308,8 @@ const DataRow = ({ item }: { item: EcrDisplay }) => {
<br />
<div>{"DOB: " + item.patient_date_of_birth || ""}</div>
</td>
<td>
{createDateDate}
<br />
{createDateTime}
</td>
<td>
{patientReportDate}
<br />
{patientReportTime}
</td>
<td>{item.date_created}</td>
<td>{item.patient_report_date || noData}</td>
<td>{conditionsList}</td>
<td>{summariesList}</td>
</tr>
Expand Down Expand Up @@ -367,30 +353,3 @@ const BlobRow = ({ themeColor }: { themeColor: string }) => {
</tr>
);
};

/**
* Formats a date object to a string in the format MM/DD/YYYY.
* @param date - The date object to be formatted.
* @returns A string in the format MM/DD/YYYY.
*/
const formatDate = (date: Date) => {
return date.toLocaleDateString("en-US");
};

/**
* Formats a date object to a string in the format HH:MM AM/PM.
* @param date - The date object to be formatted.
* @returns A string in the format HH:MM AM/PM.
*/
const formatTime = (date: Date) => {
let hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? "PM" : "AM";

hours = hours % 12;
hours = hours ? hours : 12;

const minutesStr = minutes < 10 ? `0${minutes}` : minutes;

return `${hours}:${minutesStr} ${ampm}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
TableRow,
formatAddress,
formatContactPoint,
formatDate,
formatName,
formatPhoneNumber,
formatStartEndDate,
Expand Down Expand Up @@ -304,7 +305,10 @@ export const evaluateDemographicsData = (
title: "Patient Name",
value: evaluatePatientName(fhirBundle, mappings, false),
},
{ title: "DOB", value: evaluate(fhirBundle, mappings.patientDOB)[0] },
{
title: "DOB",
value: formatDate(evaluate(fhirBundle, mappings.patientDOB)[0]),
},
{
title: "Current Age",
value: calculatePatientAge(fhirBundle, mappings)?.toString(),
Expand Down
195 changes: 105 additions & 90 deletions containers/ecr-viewer/src/app/services/formatService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,64 @@ export const formatAddress = (
.join("\n");
};

// Determine if we can extract out the timezone part of the datetime string.
const hasTimeZoneString = (dateTimeString: string): boolean => {
// Z
if (dateTimeString.match(/[0-9]Z$/)) return true;

// EDT, GMT-5, UTC+3
if (
dateTimeString
.split(/\s/)
.at(-1)
?.match(/^[A-Z]{3,}/)
)
return true;

// +/-nn[:?[nn]]
if (dateTimeString.match(/[+-]\d{1,2}:?\d{2}$/)) return true;

return false;
};

// yyyymmdd[hhmmss][+-zzzz] to ISO
const reformatNumericTimestampToISO = (dateTimeString: string) => {
// datetime is 20240101[1234][56][-0400] style
const parts = dateTimeString.match(
/(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?([+-]\d{4})?/,
);
// The regex didn't consume everything
if (!parts) {
return dateTimeString;
}
const dateParts = parts.slice(1, 4);
const timeParts = parts.slice(4, 7).filter(Boolean);
const tzPart = parts.at(-1);

let newDateStr = dateParts.join("-");
if (timeParts.length > 0) {
newDateStr += "T";
newDateStr += timeParts.join(":");
}
if (tzPart) {
newDateStr += tzPart;
}
return newDateStr;
};

/**
* Format a datetime string to "MM/DD/YYYY HH:MM AM/PM Z" where "Z" is the timezone abbreviation.If
* the input string contains a UTC offset then the returned string will be in the format
* "MM/DD/YYYY HH:MM AM/PM ±HH:MM". If the input string do not contain a time part, the returned
* "MM/DD/YYYY HH:MM AM/PM ZZZ". If there is no timezone indicated on the input date string, none will
* be returned on the output. If the input string do not contain a time part, the returned
* string will be in the format "MM/DD/YYYY". If the input string is not in the expected format, it
* will be returned as is. If the input is falsy a blank string will be returned. The following
* formats are supported:
* - "YYYY-MM-DDTHH:MM±HH:MM"
* - "YYYY-MM-DDTHH:MMZ"
* - "YYYY-MM-DD"
* - "MM/DD/YYYY HH:MM AM/PM ±HH:MM"
* - "YYYYMMDDHHMMSS±HHMM"
* @param dateTimeString datetime string.
* @returns Formatted datetime string.
*/
Expand All @@ -112,81 +159,45 @@ export const formatDateTime = (dateTimeString: string | undefined): string => {
return "";
}

// This is roughly the format that we want to convert to, therefore we can return it as is.
const customFormatRegex = /^\d{2}\/\d{2}\/\d{4} \d\d?:\d{2} [AP]M \w{3}$/;
const isoDateTimeRegex =
/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})?)?/;
if (customFormatRegex.test(dateTimeString)) {
return dateTimeString;
} else if (isoDateTimeRegex.test(dateTimeString)) {
// Split the datetime string into date and time parts
const [datePart, timePart] = dateTimeString.split("T");

// Further split the date part into YYYY, MM, DD
const [year, month, day] = datePart.split("-");

if (timePart) {
// Split the time part into HH:MM:SS and timezone (±HH:MM)
const [time, timeZone] = timePart.split(/(?=[+-])/);

// We only need HH:MM from the time
const [hours, minutes] = time.split(":");

// Convert 24-hour time to 12-hour time
const hoursInt = parseInt(hours, 10);
const suffix = hoursInt >= 12 ? "PM" : "AM";
const hours12 = ((hoursInt + 11) % 12) + 1; // Convert 24h to 12h format

const formattedDateTime = `${month}/${day}/${year} ${hours12}:${minutes} ${suffix} ${
timeZone || "UTC"
}`;
return formattedDateTime;
const date = new Date(dateTimeString);
if (date.toString() === "Invalid Date") {
// datetime is 20240101[1234][56][-0400] style?
const newDateStr = reformatNumericTimestampToISO(dateTimeString);
if (newDateStr !== dateTimeString) {
return formatDateTime(newDateStr);
}

// Reformat the string as needed
const formattedDate = `${month}/${day}/${year}`;
return formattedDate;
// If we are unable to format the date, return as is
return dateTimeString;
}

// If the input string is not in the expected format, return it as is
return dateTimeString;
};
// time as 00:00[:000]
const hasTime = dateTimeString.includes(":");
if (!hasTime) {
return formatDate(dateTimeString) || dateTimeString;
}

/**
* Converts a UTC date time string to the date string in the user's local timezone.
* Returned date is in format "MM/DD/YYYY HH:MM AM/PM Z" where "Z" is the timezone abbreviation
* @param utcDateString - The date string in UTC to be converted.
* @returns The formatted date string converted to the user's local timezone.
* @throws {Error} If the input UTC date string is invalid.
*/
export function convertUTCToLocalString(utcDateString: string): string {
const utcDate = new Date(utcDateString);
if (isNaN(utcDate.getTime())) {
throw new Error("Invalid UTC date string");
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
};
const formatted = (date as unknown as Date)
.toLocaleDateString("en-Us", options)
.replace(",", "");

// Not actually time zoned
if (!hasTimeZoneString(dateTimeString)) {
return formatted.slice(0, formatted.lastIndexOf(" ")); // lop off " EDT"
}

const timeZoneAbbr = utcDate
.toLocaleString("en-US", {
timeZoneName: "short",
})
.split(" ")[3]; // Extract the third part which is the abbreviated timezone

const formattedDateString =
utcDate
.toLocaleString("en-US", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
.replace(",", "") +
" " +
timeZoneAbbr;

return formattedDateString;
}
// encourage word wrapping between date and time instead of wherever
const parts = formatted.split(" ");
return `${parts[0]} ${parts.slice(1).join("\u00A0")}`;
};

/**
* Formats the provided date string into a formatted date string with year, month, and day.
Expand All @@ -197,22 +208,21 @@ export const formatDate = (dateString?: string): string | undefined => {
if (dateString) {
let date = new Date(dateString);

if (date.toString() == "Invalid Date") {
const formattedDate = `${dateString.substring(
0,
4,
)}-${dateString.substring(4, 6)}-${dateString.substring(6, 8)}`; // yyyy-mm-dd
date = new Date(formattedDate);
}
// double check that the reformat actually worked otherwise return nothing
if (date.toString() != "Invalid Date") {
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
timeZone: "UTC",
}); // UTC, otherwise will have timezone issues
if (date.toString() === "Invalid Date") {
const newDateStr = reformatNumericTimestampToISO(dateString);
if (newDateStr !== dateString) {
return formatDate(newDateStr);
}

return undefined;
}

return date.toLocaleDateString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
timeZone: "UTC",
}); // UTC, otherwise will have timezone issues
}
};

Expand Down Expand Up @@ -499,19 +509,24 @@ function processTable(table: Element): TableRow[] {
row.querySelectorAll("td").forEach((cell, cellIndex) => {
const key = hasHeaders ? keys[cellIndex] : "Unknown Header";

const metaData: Metadata = {};
const metadata: Metadata = {};
const attributes = cell.attributes || [];
for (const element of attributes) {
const attrName = element.nodeName;
const attrValue = element.nodeValue;
if (attrName && attrValue) {
metaData[attrName] = attrValue;
metadata[attrName] = attrValue;
}
}
obj[key] = {
value: getElementContent(cell),
metadata: metaData,
};
let value = getElementContent(cell);
if (
typeof value === "string" &&
(key.toLowerCase().includes("date") ||
key.toLowerCase().includes("time"))
) {
value = formatDateTime(value);
}
obj[key] = { value, metadata };
});
jsonArray.push(obj);
});
Expand Down
Loading

0 comments on commit 5cb4673

Please sign in to comment.