diff --git a/scope_shared/scope/schemas/datetime.json b/scope_shared/scope/schemas/datetime.json index 042e7ea9..d61f79c7 100644 --- a/scope_shared/scope/schemas/datetime.json +++ b/scope_shared/scope/schemas/datetime.json @@ -5,10 +5,10 @@ "type": "object", "properties": { "date": { - "type": "string", - "pattern": "^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T00:00:00(|.[0]*)Z$", - "$comment": "Allows 'YYYY-MM-DDT00:00:00Z' and 'YYYY-MM-DDT00:00:00.[0]*Z'" - }, + "type": "string", + "pattern": "^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "$comment": "Allows 'YYYY-MM-DDT00:00:00Z' and 'YYYY-MM-DDT00:00:00.[0]*Z'" + }, "datetime": { "type": "string", "pattern": "^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(|.[0-9]*)Z$", diff --git a/web_registry/src/components/PatientDetail/PatientCard.tsx b/web_registry/src/components/PatientDetail/PatientCard.tsx index b9b4a7b5..a86b8080 100644 --- a/web_registry/src/components/PatientDetail/PatientCard.tsx +++ b/web_registry/src/components/PatientDetail/PatientCard.tsx @@ -1,10 +1,10 @@ import EditIcon from '@mui/icons-material/Edit'; import { Button, Divider, Grid, LinearProgress, Snackbar, Typography } from '@mui/material'; import withTheme from '@mui/styles/withTheme'; +import { format } from 'date-fns'; import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import React, { FunctionComponent, useEffect, useState } from 'react'; -import { formatDateOnly } from 'shared/time'; import { IPatientProfile, KeyedMap } from 'shared/types'; import LabeledField from 'src/components/common/LabeledField'; import { EditPatientProfileDialog } from 'src/components/PatientDetail/PatientProfileDialog'; @@ -118,9 +118,7 @@ export const PatientCard: FunctionComponent = observer((props = 0 ? patient.age : '--'} /> diff --git a/web_registry/src/components/PatientDetail/PatientProfileDialog.tsx b/web_registry/src/components/PatientDetail/PatientProfileDialog.tsx index 556a5769..eae8ce2b 100644 --- a/web_registry/src/components/PatientDetail/PatientProfileDialog.tsx +++ b/web_registry/src/components/PatientDetail/PatientProfileDialog.tsx @@ -12,7 +12,6 @@ import { patientRaceValues, patientSexValues, } from 'shared/enums'; -import { toLocalDateOnly, toUTCDateOnly } from 'shared/time'; import { IPatientProfile, IProviderIdentity } from 'shared/types'; import { GridDateField, GridDropdownField, GridMultiSelectField, GridTextField } from 'src/components/common/GridField'; import StatefulDialog from 'src/components/common/StatefulDialog'; @@ -187,28 +186,14 @@ export const AddPatientProfileDialog: FunctionComponent = observer((props) => { const { profile, open, error, loading, onClose, onSavePatient, careManagers } = props; - const state = useLocalObservable(() => { - const existingProfile = { ...profile }; - - if (profile.birthdate != undefined) { - existingProfile.birthdate = toLocalDateOnly(profile.birthdate); - } - - return existingProfile; - }); + const state = useLocalObservable(() => ({ ...profile })); const onValueChange = action((key: string, value: any) => { (state as any)[key] = value; }); const onSave = action(() => { - const updatedProfile = { ...state }; - - if (state.birthdate != undefined) { - updatedProfile.birthdate = toUTCDateOnly(state.birthdate); - } - - onSavePatient(updatedProfile); + onSavePatient({ ...state }); }); const onCareManagerChange = action((name: string) => { diff --git a/web_registry/src/components/PatientDetail/SessionInfo.tsx b/web_registry/src/components/PatientDetail/SessionInfo.tsx index fa81f757..02adee59 100644 --- a/web_registry/src/components/PatientDetail/SessionInfo.tsx +++ b/web_registry/src/components/PatientDetail/SessionInfo.tsx @@ -14,7 +14,7 @@ import { referralStatusValues, sessionTypeValues, } from 'shared/enums'; -import { toLocalDateOnly, toUTCDateOnly, clearTime } from 'shared/time'; +import { clearTime } from 'shared/time'; import { ICaseReview, IReferralStatus, ISession, ISessionOrCaseReview, KeyedMap } from 'shared/types'; import ActionPanel, { IActionButton } from 'src/components/common/ActionPanel'; import { @@ -406,9 +406,6 @@ export const SessionInfo: FunctionComponent = observer(() => { state.open = true; state.isNew = false; state.entryType = 'Session'; - if (!!session && session.date) { - state.session.date = toLocalDateOnly(session.date); - } _copySessionToState(session); }); @@ -420,9 +417,6 @@ export const SessionInfo: FunctionComponent = observer(() => { state.open = true; state.isNew = false; state.entryType = 'Case Review'; - if (!!review && review.date) { - state.review.date = toLocalDateOnly(review.date); - } }); const onSave = action(async () => { @@ -451,7 +445,6 @@ export const SessionInfo: FunctionComponent = observer(() => { ) .filter((rs) => rs.referralStatus != 'Not Referred'); - updatedSession.date = toUTCDateOnly(session.date); updatedSession.billableMinutes = Number(session.billableMinutes); if (isNew) { @@ -461,7 +454,6 @@ export const SessionInfo: FunctionComponent = observer(() => { } } else if (entryType == 'Case Review') { const updatedReview = { ...review }; - updatedReview.date = toUTCDateOnly(review.date); if (isNew) { await currentPatient.addCaseReview(updatedReview); diff --git a/web_registry/src/components/PatientDetail/SessionReviewTable.tsx b/web_registry/src/components/PatientDetail/SessionReviewTable.tsx index 273ad335..7fe5050f 100644 --- a/web_registry/src/components/PatientDetail/SessionReviewTable.tsx +++ b/web_registry/src/components/PatientDetail/SessionReviewTable.tsx @@ -1,9 +1,9 @@ import { Grid } from '@mui/material'; import withTheme from '@mui/styles/withTheme'; import { GridCellParams, GridColDef, GridColumnHeaderParams, GridRowParams } from '@mui/x-data-grid'; +import { format } from 'date-fns'; import React, { FunctionComponent } from 'react'; import { CaseReviewEntryType, SessionType } from 'shared/enums'; -import { formatDateOnly } from 'shared/time'; import { ICaseReview, IReferralStatus, ISession, ISessionOrCaseReview, isSession, KeyedMap } from 'shared/types'; import { Table } from 'src/components/common/Table'; import styled from 'styled-components'; @@ -161,7 +161,7 @@ export const SessionReviewTable: FunctionComponent = ( const getSessionData = (session: ISession): ISessionTableData => ({ id: session.sessionId || '--', - date: `${formatDateOnly(session.date, 'MM/dd/yy')}`, + date: `${format(session.date, 'MM/dd/yy')}`, type: session.sessionType, billableMinutes: session.billableMinutes, flag: 'TBD', @@ -174,7 +174,7 @@ export const SessionReviewTable: FunctionComponent = ( const getReviewData = (review: ICaseReview): ISessionTableData => ({ id: review.caseReviewId || '--', - date: `${formatDateOnly(review.date, 'MM/dd/yy')}`, + date: `${format(review.date, 'MM/dd/yy')}`, type: 'Case Review', billableMinutes: NA, flag: 'TBD', diff --git a/web_registry/src/components/PatientDetail/TreatmentInfo.tsx b/web_registry/src/components/PatientDetail/TreatmentInfo.tsx index a3a78bff..c6f49017 100644 --- a/web_registry/src/components/PatientDetail/TreatmentInfo.tsx +++ b/web_registry/src/components/PatientDetail/TreatmentInfo.tsx @@ -21,7 +21,7 @@ export const TreatmentInfo: FunctionComponent = observer(() => { const latestGadScore = getLatestScore(currentPatient?.assessmentLogs, 'gad-7'); const latestSessionDate = currentPatient.latestSession?.date; - const currentMedications = currentPatient.latestSession?.currentMedications; + const currentMedications = currentPatient.latestSession?.currentMedications || ''; const behavioralStrategiesUsedFlags: BehavioralStrategyChecklistFlags = { 'Behavioral Activation': false, 'Motivational Interviewing': false, @@ -49,9 +49,10 @@ export const TreatmentInfo: FunctionComponent = observer(() => { const behavioralStrategiesUsed = behavioralStrategiesUsedList.join('\n'); - const referrals = currentPatient.latestSession?.referrals - .map((ref) => `${ref.referralType} - ${ref.referralStatus}`) - .join('\n'); + const referrals = + currentPatient.latestSession?.referrals + .map((ref) => `${ref.referralType} - ${ref.referralStatus}`) + .join('\n') || ''; const loading = diff --git a/web_shared/package.json b/web_shared/package.json index d8362354..589432df 100644 --- a/web_shared/package.json +++ b/web_shared/package.json @@ -5,7 +5,6 @@ "amazon-cognito-identity-js": "^5.2.7", "axios": "^0.21.1", "date-fns": "^2.17.0", - "date-fns-tz": "^1.2.2", "lodash": "^4.17.21", "lorem-ipsum": "^2.0.3", "mobx": "6.0.x" diff --git a/web_shared/serviceBase.ts b/web_shared/serviceBase.ts index 38613563..bd5c2194 100644 --- a/web_shared/serviceBase.ts +++ b/web_shared/serviceBase.ts @@ -1,5 +1,5 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; -import { handleDates } from 'shared/time'; +import { handleRequestDates, handleResponseDates } from 'shared/time'; import { KeyedMap } from 'shared/types'; export interface IServiceBase { @@ -36,7 +36,7 @@ export class ServiceBase implements IServiceBase { }; const handleResponse = (response: AxiosResponse) => { - handleDates(response.data); + handleResponseDates(response.data); handleDocuments(response.data); return response; @@ -44,7 +44,7 @@ export class ServiceBase implements IServiceBase { const handleError = (error: AxiosError) => { if (error.response?.status == 409 && error.response != undefined) { - handleDates(error.response?.data); + handleResponseDates(error.response?.data); handleDocuments(error.response?.data); } @@ -62,6 +62,9 @@ export class ServiceBase implements IServiceBase { delete request.data[docName]._id; } } + + handleRequestDates(request.data); + return request; }; diff --git a/web_shared/time.ts b/web_shared/time.ts index a0fff59c..95713b3c 100644 --- a/web_shared/time.ts +++ b/web_shared/time.ts @@ -1,48 +1,59 @@ -import { format, parseISO, setHours, setMilliseconds, setMinutes, setSeconds } from 'date-fns'; -import { utcToZonedTime } from 'date-fns-tz'; +import { format, parse, parseISO, setHours, setMilliseconds, setMinutes, setSeconds } from 'date-fns'; import { FollowupSchedule } from 'shared/enums'; export const clearTime = (date: Date) => { return setMilliseconds(setSeconds(setMinutes(setHours(date, 0), 0), 0), 0); }; -// Takes the "date" type from service and converts it to a formatted date only string -export const formatDateOnly = (date: Date, formatter: string = 'MM/dd/yy') => { - return format(toLocalDateOnly(date), formatter); +// TODO: Remove after migration is complete +const isoDateTimeFormat = + /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(|.[0-9]*)$/; +const isoDateTimeFormatNew = + /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(|.[0-9]*)Z$/; +const justDateFormat = /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; +const dateFormat = 'yyyy-MM-dd'; +const dateFieldSuffix = 'date'; + +const isIsoDateTimeString = (value: any): boolean => { + return !!value && typeof value === 'string' && (isoDateTimeFormat.test(value) || isoDateTimeFormatNew.test(value)); }; -// Takes the "date" type from service and converts it to a local date only object -export const toLocalDateOnly = (date: Date) => { - return utcToZonedTime(date, '+00'); +const isJustDateString = (value: any): boolean => { + return !!value && typeof value === 'string' && justDateFormat.test(value); }; -// Takes the local date only object and converts to service's "date" type -export const toUTCDateOnly = (date: Date) => { - // TODO: Investigate new Intl APIs for extracting current timezones - return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0)); +const isInstanceOfDate = (value: any): boolean => { + return Object.prototype.toString.call(value) === '[object Date]'; }; -// TODO: Remove after migration is complete -const isoDateFormat = - /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(|.[0-9]*)$/; -const isoDateFormatNew = - /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(|.[0-9]*)Z$/; +export const handleResponseDates = (body: any) => { + if (body === null || body === undefined || typeof body !== 'object') { + return body; + } -const isIsoDateString = (value: any): boolean => { - return !!value && typeof value === 'string' && (isoDateFormat.test(value) || isoDateFormatNew.test(value)); + for (const key of Object.keys(body)) { + const value = body[key]; + if (isIsoDateTimeString(value)) { + body[key] = parseISO(value); + } else if (isJustDateString(value)) { + body[key] = parse(value, dateFormat, new Date(0, 0, 0, 0, 0, 0)); + } else if (typeof value === 'object') { + handleResponseDates(value); + } + } }; -export const handleDates = (body: any) => { +export const handleRequestDates = (body: any) => { if (body === null || body === undefined || typeof body !== 'object') { return body; } for (const key of Object.keys(body)) { const value = body[key]; - if (isIsoDateString(value)) { - body[key] = parseISO(value); + if (isInstanceOfDate(value) && key.toLowerCase().endsWith(dateFieldSuffix)) { + body[key] = format(value, dateFormat); } else if (typeof value === 'object') { - handleDates(value); + handleRequestDates(value); } } }; diff --git a/web_shared/yarn.lock b/web_shared/yarn.lock index 537b2b95..38d7693b 100644 --- a/web_shared/yarn.lock +++ b/web_shared/yarn.lock @@ -49,11 +49,6 @@ crypto-js@^4.1.1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== -date-fns-tz@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.2.2.tgz#89432b54ce3fa7d050a2039e997e5b6a96df35dd" - integrity sha512-vWtn44eEqnLbkACb7T5G5gPgKR4nY8NkNMOCyoY49NsRGHrcDmY2aysCyzDeA+u+vcDBn/w6nQqEDyouRs4m8w== - date-fns@^2.17.0: version "2.27.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.27.0.tgz#e1ff3c3ddbbab8a2eaadbb6106be2929a5a2d92b"