MED-82: add patient notification emails (#74)
* MED-82: add patient notification emails * remove console.log * clean up * remove extra paragraph from email
This commit is contained in:
@@ -23,7 +23,7 @@ export const POST = async (request: NextRequest) => {
|
||||
'Successfully sent out open job notification emails to doctors.',
|
||||
);
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.NEW_JOBS_ALERT,
|
||||
action: NotificationAction.DOCTOR_NEW_JOBS,
|
||||
status: 'SUCCESS',
|
||||
});
|
||||
return NextResponse.json(
|
||||
@@ -39,7 +39,7 @@ export const POST = async (request: NextRequest) => {
|
||||
e,
|
||||
);
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.NEW_JOBS_ALERT,
|
||||
action: NotificationAction.DOCTOR_NEW_JOBS,
|
||||
status: 'FAIL',
|
||||
comment: e?.message,
|
||||
});
|
||||
|
||||
@@ -24,9 +24,9 @@ export default async function AnalysisResultsPage({
|
||||
}) {
|
||||
const account = await loadCurrentUserAccount();
|
||||
|
||||
const { id: analysisResponseId } = await params;
|
||||
const { id: analysisOrderId } = await params;
|
||||
|
||||
const analysisResponse = await loadUserAnalysis(Number(analysisResponseId));
|
||||
const analysisResponse = await loadUserAnalysis(Number(analysisOrderId));
|
||||
|
||||
if (!account?.id || !analysisResponse) {
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Tables } from '@/packages/supabase/src/database.types';
|
||||
|
||||
import { AccountWithParams } from '@kit/accounts/api';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
@@ -26,6 +25,19 @@ export async function getAccount(id: string): Promise<AccountWithMemberships> {
|
||||
return data as unknown as AccountWithMemberships;
|
||||
}
|
||||
|
||||
export async function getUserContactAdmin(userId: string) {
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('name, last_name, email, preferred_locale')
|
||||
.eq('primary_owner_user_id', userId)
|
||||
.eq('is_personal_account', true)
|
||||
.single()
|
||||
.throwOnError();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getAccountAdmin({
|
||||
primaryOwnerUserId,
|
||||
}: {
|
||||
|
||||
@@ -2,9 +2,12 @@ import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
export enum NotificationAction {
|
||||
DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED',
|
||||
NEW_JOBS_ALERT = 'NEW_JOBS_ALERT',
|
||||
PATIENT_RESULTS_RECEIVED_ALERT = 'PATIENT_RESULTS_RECEIVED_ALERT',
|
||||
DOCTOR_NEW_JOBS = 'DOCTOR_NEW_JOBS',
|
||||
DOCTOR_PATIENT_RESULTS_RECEIVED = 'DOCTOR_PATIENT_RESULTS_RECEIVED',
|
||||
PATIENT_DOCTOR_FEEDBACK_RECEIVED = 'PATIENT_DOCTOR_FEEDBACK_RECEIVED',
|
||||
PATIENT_ORDER_PROCESSING = 'PATIENT_ORDER_PROCESSING',
|
||||
PATIENT_FIRST_RESULTS_RECEIVED = 'PATIENT_FIRST_RESULTS_RECEIVED',
|
||||
PATIENT_FULL_RESULTS_RECEIVED = 'PATIENT_FULL_RESULTS_RECEIVED',
|
||||
}
|
||||
|
||||
export const createNotificationLog = async ({
|
||||
|
||||
@@ -37,7 +37,6 @@ export const createPageViewLog = async ({
|
||||
account_id: accountId,
|
||||
action,
|
||||
changed_by: user.id,
|
||||
extra_data: extraData,
|
||||
})
|
||||
.throwOnError();
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,7 +13,7 @@ type EmailTemplate = {
|
||||
subject: string;
|
||||
};
|
||||
|
||||
type EmailRenderer<T = any> = (params: T) => Promise<EmailTemplate>;
|
||||
export type EmailRenderer<T = any> = (params: T) => Promise<EmailTemplate>;
|
||||
|
||||
export const sendEmailFromTemplate = async <T>(
|
||||
renderer: EmailRenderer<T>,
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import {
|
||||
renderAllResultsReceivedEmail,
|
||||
renderFirstResultsReceivedEmail,
|
||||
} from '@kit/email-templates';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import {
|
||||
getAssignedDoctorAccount,
|
||||
getDoctorAccounts,
|
||||
} from '../../../../../lib/services/account.service';
|
||||
import {
|
||||
NotificationAction,
|
||||
createNotificationLog,
|
||||
} from '../../../../../lib/services/audit/notificationEntries.service';
|
||||
import { sendEmailFromTemplate } from '../../../../../lib/services/mailer.service';
|
||||
import { RecordChange, Tables } from '../record-change.type';
|
||||
|
||||
export function createDatabaseWebhookRouterService(
|
||||
@@ -113,58 +100,13 @@ class DatabaseWebhookRouterService {
|
||||
return;
|
||||
}
|
||||
|
||||
let action;
|
||||
try {
|
||||
const data = {
|
||||
analysisOrderId: record.id,
|
||||
language: 'et',
|
||||
};
|
||||
const { createAnalysisOrderWebhooksService } = await import(
|
||||
'@kit/notifications/webhooks/analysis-order-notifications.service'
|
||||
);
|
||||
|
||||
if (record.status === 'PARTIAL_ANALYSIS_RESPONSE') {
|
||||
action = NotificationAction.NEW_JOBS_ALERT;
|
||||
const service = createAnalysisOrderWebhooksService();
|
||||
|
||||
const doctorAccounts = await getDoctorAccounts();
|
||||
const doctorEmails: string[] = doctorAccounts
|
||||
.map(({ email }) => email)
|
||||
.filter((email): email is string => !!email);
|
||||
|
||||
await sendEmailFromTemplate(
|
||||
renderFirstResultsReceivedEmail,
|
||||
data,
|
||||
doctorEmails,
|
||||
);
|
||||
} else if (record.status === 'FULL_ANALYSIS_RESPONSE') {
|
||||
action = NotificationAction.PATIENT_RESULTS_RECEIVED_ALERT;
|
||||
const doctorAccount = await getAssignedDoctorAccount(record.id);
|
||||
const assignedDoctorEmail = doctorAccount?.email;
|
||||
|
||||
if (!assignedDoctorEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendEmailFromTemplate(
|
||||
renderAllResultsReceivedEmail,
|
||||
data,
|
||||
assignedDoctorEmail,
|
||||
);
|
||||
}
|
||||
|
||||
if (action) {
|
||||
await createNotificationLog({
|
||||
action,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: record.id,
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (action)
|
||||
await createNotificationLog({
|
||||
action,
|
||||
status: 'FAIL',
|
||||
comment: e?.message,
|
||||
relatedRecordId: record.id,
|
||||
});
|
||||
}
|
||||
return service.handleStatusChangeWebhook(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,33 +49,28 @@ export async function renderAccountDeleteEmail(props: Props) {
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`, {
|
||||
displayName: props.userDisplayName,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph1`, {
|
||||
productName: props.productName,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph2`)}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph3`, {
|
||||
productName: props.productName,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph4`, {
|
||||
productName: props.productName,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
@@ -46,11 +46,10 @@ export async function renderAllResultsReceivedEmail({
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`)}
|
||||
</Text>
|
||||
@@ -62,7 +61,6 @@ export async function renderAllResultsReceivedEmail({
|
||||
>
|
||||
{t(`${namespace}:linkText`)}
|
||||
</EmailButton>
|
||||
|
||||
<Text>
|
||||
{t(`${namespace}:ifLinksDisabled`)}{' '}
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
|
||||
|
||||
@@ -55,23 +55,19 @@ export async function renderCompanyOfferEmail({
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:companyName`)} {companyData.companyName}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:contactPerson`)} {companyData.contactPerson}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:email`)} {companyData.email}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:phone`)} {companyData.phone || 'N/A'}
|
||||
</Text>
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Link,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import CommonFooter from '../components/common-footer';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { EmailButton } from '../components/email-button';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
@@ -20,12 +20,10 @@ import { initializeEmailI18n } from '../lib/i18n';
|
||||
export async function renderDoctorSummaryReceivedEmail({
|
||||
language,
|
||||
recipientName,
|
||||
orderNr,
|
||||
analysisOrderId,
|
||||
}: {
|
||||
language?: string;
|
||||
language: string;
|
||||
recipientName: string;
|
||||
orderNr: string;
|
||||
analysisOrderId: number;
|
||||
}) {
|
||||
const namespace = 'doctor-summary-received-email';
|
||||
@@ -35,13 +33,9 @@ export async function renderDoctorSummaryReceivedEmail({
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
orderNr,
|
||||
});
|
||||
const previewText = t(`${namespace}:previewText`);
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
orderNr,
|
||||
});
|
||||
const subject = t(`${namespace}:subject`);
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
@@ -54,29 +48,26 @@ export async function renderDoctorSummaryReceivedEmail({
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`, {
|
||||
displayName: recipientName,
|
||||
})}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:summaryReceivedForOrder`, { orderNr })}
|
||||
{t(`common:helloName`, { name: recipientName })}
|
||||
</Text>
|
||||
|
||||
<EmailButton
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
>
|
||||
{t(`${namespace}:linkText`, { orderNr })}
|
||||
</EmailButton>
|
||||
<Text>
|
||||
{t(`${namespace}:ifButtonDisabled`)}{' '}
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
{t(`${namespace}:p1`)}{' '}
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
>
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
</Link>
|
||||
</Text>
|
||||
<Text>{t(`${namespace}:p2`)}</Text>
|
||||
<Text>{t(`${namespace}:p3`)}</Text>
|
||||
<Text>{t(`${namespace}:p4`)}</Text>
|
||||
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
|
||||
@@ -46,11 +46,10 @@ export async function renderFirstResultsReceivedEmail({
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`)}
|
||||
</Text>
|
||||
|
||||
@@ -74,20 +74,17 @@ export async function renderInviteEmail(props: Props) {
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{hello}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className="text-[16px] leading-[24px] text-[#242424]"
|
||||
dangerouslySetInnerHTML={{ __html: mainText }}
|
||||
/>
|
||||
|
||||
{props.teamLogo && (
|
||||
<Section>
|
||||
<Row>
|
||||
@@ -102,20 +99,16 @@ export async function renderInviteEmail(props: Props) {
|
||||
</Row>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section className="mb-[32px] mt-[32px] text-center">
|
||||
<Section className="mt-[32px] mb-[32px] text-center">
|
||||
<CtaButton href={props.link}>{joinTeam}</CtaButton>
|
||||
</Section>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:copyPasteLink`)}{' '}
|
||||
<Link href={props.link} className="text-blue-600 no-underline">
|
||||
{props.link}
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
|
||||
|
||||
<Text className="text-[12px] leading-[24px] text-[#666666]">
|
||||
{t(`${namespace}:invitationIntendedFor`, {
|
||||
invitedUserEmail: props.invitedUserEmail,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
@@ -50,11 +50,10 @@ export async function renderNewJobsAvailableEmail({
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`)}
|
||||
</Text>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import CommonFooter from '../components/common-footer';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
export async function renderOrderProcessingEmail({
|
||||
language,
|
||||
recipientName,
|
||||
partnerLocation,
|
||||
isUrine,
|
||||
}: {
|
||||
language: string;
|
||||
recipientName: string;
|
||||
partnerLocation: string;
|
||||
isUrine?: boolean;
|
||||
}) {
|
||||
const namespace = 'order-processing-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language,
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`);
|
||||
|
||||
const subject = t(`${namespace}:subject`);
|
||||
|
||||
const p2 = t(`${namespace}:p2`);
|
||||
const p4 = t(`${namespace}:p4`);
|
||||
const p1Urine = t(`${namespace}:p1Urine`);
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`common:helloName`, { name: recipientName })}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] font-semibold text-[#242424]">
|
||||
{t(`${namespace}:heading`)}
|
||||
</Text>
|
||||
<Text>{t(`${namespace}:p1`, { partnerLocation })}</Text>
|
||||
<Text dangerouslySetInnerHTML={{ __html: p2 }}></Text>
|
||||
<Text>{t(`${namespace}:p3`)}</Text>
|
||||
<Text dangerouslySetInnerHTML={{ __html: p4 }}></Text>
|
||||
{isUrine && (
|
||||
<>
|
||||
<Text dangerouslySetInnerHTML={{ __html: p1Urine }}></Text>
|
||||
<Text>{t(`${namespace}:p2Urine`)}</Text>
|
||||
</>
|
||||
)}
|
||||
<Text>{t(`${namespace}:p5`)}</Text>
|
||||
<Text>{t(`${namespace}:p6`)}</Text>
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
@@ -60,18 +60,17 @@ export async function renderOtpEmail(props: Props) {
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] text-[#242424]">{mainText}</Text>
|
||||
|
||||
<Text className="text-[16px] text-[#242424]">{otpText}</Text>
|
||||
|
||||
<Section className="mb-[16px] mt-[16px] text-center">
|
||||
<Section className="mt-[16px] mb-[16px] text-center">
|
||||
<Button className={'w-full rounded bg-neutral-950 text-center'}>
|
||||
<Text className="text-[16px] font-semibold leading-[16px] text-white">
|
||||
<Text className="text-[16px] leading-[16px] font-semibold text-white">
|
||||
{props.otp}
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Link,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import CommonFooter from '../components/common-footer';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
export async function renderPatientFirstResultsReceivedEmail({
|
||||
language,
|
||||
recipientName,
|
||||
analysisOrderId,
|
||||
}: {
|
||||
language: string;
|
||||
recipientName: string;
|
||||
analysisOrderId: number;
|
||||
}) {
|
||||
const namespace = 'patient-first-results-received-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language,
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`);
|
||||
|
||||
const subject = t(`${namespace}:subject`);
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`common:helloName`, { name: recipientName })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t(`${namespace}:p1`)}{' '}
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
>
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
</Link>
|
||||
</Text>
|
||||
<Text>{t(`${namespace}:p2`)}</Text>
|
||||
<Text>{t(`${namespace}:p3`)}</Text>
|
||||
<Text>{t(`${namespace}:p4`)}</Text>
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Link,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import CommonFooter from '../components/common-footer';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
export async function renderPatientFullResultsReceivedEmail({
|
||||
language,
|
||||
recipientName,
|
||||
analysisOrderId,
|
||||
}: {
|
||||
language: string;
|
||||
recipientName: string;
|
||||
analysisOrderId: number;
|
||||
}) {
|
||||
const namespace = 'patient-full-results-received-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language,
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`);
|
||||
|
||||
const subject = t(`${namespace}:subject`);
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`common:helloName`, { name: recipientName })}
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
{t(`${namespace}:p1`)}{' '}
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
>
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||
</Link>
|
||||
</Text>
|
||||
<Text>{t(`${namespace}:p2`)}</Text>
|
||||
<Text>{t(`${namespace}:p3`)}</Text>
|
||||
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) {
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
analysisPackageName: props.analysisPackageName,
|
||||
});
|
||||
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
analysisPackageName: props.analysisPackageName,
|
||||
});
|
||||
@@ -70,15 +70,13 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) {
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{hello}
|
||||
</Text>
|
||||
|
||||
{lines.map((line, index) => (
|
||||
<Text
|
||||
key={index}
|
||||
@@ -86,7 +84,6 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) {
|
||||
dangerouslySetInnerHTML={{ __html: line }}
|
||||
/>
|
||||
))}
|
||||
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
|
||||
@@ -7,3 +7,6 @@ export * from './emails/doctor-summary-received.email';
|
||||
export * from './emails/new-jobs-available.email';
|
||||
export * from './emails/first-results-received.email';
|
||||
export * from './emails/all-results-received.email';
|
||||
export * from './emails/order-processing.email';
|
||||
export * from './emails/patient-first-results-received.email';
|
||||
export * from './emails/patient-full-results-received.email';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"subject": "Doctor feedback to order {{orderNr}} received",
|
||||
"previewText": "A doctor has submitted feedback on your analysis results.",
|
||||
"hello": "Hello {{displayName}},",
|
||||
"summaryReceivedForOrder": "A doctor has submitted feedback to your analysis results from order {{orderNr}}.",
|
||||
"linkText": "View summary",
|
||||
"ifButtonDisabled": "If clicking the button does not work, copy this link to your browser's url field:"
|
||||
}
|
||||
"subject": "Doctor's summary has arrived",
|
||||
"previewText": "The doctor has prepared a summary of the test results.",
|
||||
"p1": "The doctor's summary has arrived:",
|
||||
"p2": "It is recommended to have a comprehensive health check-up regularly, at least once a year, if you wish to maintain an active and fulfilling lifestyle.",
|
||||
"p3": "MedReport makes it easy, convenient, and fast to view health data in one place and order health check-ups.",
|
||||
"p4": "SYNLAB customer support phone: 17123"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"subject": "The referral has been sent to the laboratory. Please go to give samples.",
|
||||
"heading": "Thank you for your order!",
|
||||
"previewText": "The referral for tests has been sent to the laboratory.",
|
||||
"p1": "The referral for tests has been sent to the laboratory digitally. Please go to give samples: {{partnerLocation}}.",
|
||||
"p2": "If you are unable to go to the selected location to give samples, you may visit any other sampling point convenient for you - <a href='https://medreport.ee/et/verevotupunktid'>see locations and opening hours</a>.",
|
||||
"p3": "It is recommended to give samples preferably in the morning (before 12:00) and on an empty stomach without drinking or eating (you may drink water).",
|
||||
"p4": "At the sampling point, please choose in the queue system: under <strong>referrals</strong> select <strong>specialist referral</strong>.",
|
||||
"p5": "If you have any additional questions, please do not hesitate to contact us.",
|
||||
"p6": "SYNLAB customer support phone: 17123",
|
||||
"p1Urine": "The tests include a <strong>urine test</strong>. For the urine test, please collect the first morning urine.",
|
||||
"p2Urine": "You can buy a sample container at the pharmacy and bring the sample with you (procedure performed at home), or ask for one at the sampling point (procedure performed in the point’s restroom)."
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"subject": "The first ordered test results have arrived",
|
||||
"previewText": "The first test results have arrived.",
|
||||
"p1": "The first test results have arrived:",
|
||||
"p2": "We will send the next notification once all test results have been received in the system.",
|
||||
"p3": "If you have any additional questions, please feel free to contact us.",
|
||||
"p4": "SYNLAB customer support phone: 17123"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"subject": "All ordered test results have arrived. Awaiting doctor's summary.",
|
||||
"previewText": "All test results have arrived.",
|
||||
"p1": "All test results have arrived:",
|
||||
"p2": "We will send the next notification once the doctor's summary has been prepared.",
|
||||
"p3": "SYNLAB customer support phone: 17123"
|
||||
}
|
||||
@@ -4,5 +4,7 @@
|
||||
"lines2": "E-mail: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
|
||||
"lines3": "Klienditugi: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
|
||||
"lines4": "<a href=\"https://www.medreport.ee\">www.medreport.ee</a>"
|
||||
}
|
||||
},
|
||||
"helloName": "Tere, {{name}}",
|
||||
"hello": "Tere"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"subject": "Saabus arsti kokkuvõtte tellimusele {{orderNr}}",
|
||||
"previewText": "Arst on saatnud kokkuvõtte sinu analüüsitulemustele.",
|
||||
"hello": "Tere, {{displayName}}",
|
||||
"summaryReceivedForOrder": "Arst on koostanud selgitava kokkuvõtte sinu tellitud analüüsidele.",
|
||||
"linkText": "Vaata kokkuvõtet",
|
||||
"ifButtonDisabled": "Kui nupule vajutamine ei toimi, kopeeri see link oma brauserisse:"
|
||||
"subject": "Arsti kokkuvõte on saabunud",
|
||||
"previewText": "Arst on koostanud kokkuvõte analüüsitulemustele.",
|
||||
"p1": "Arsti kokkuvõte on saabunud:",
|
||||
"p2": "Põhjalikul terviseuuringul on soovituslik käia regulaarselt, aga vähemalt üks kord aastas, kui soovite säilitada aktiivset ja täisväärtuslikku elustiili.",
|
||||
"p3": "MedReport aitab lihtsalt, mugavalt ja kiirelt terviseandmeid ühest kohast vaadata ning tellida terviseuuringuid.",
|
||||
"p4": "SYNLAB klienditoe telefon: 17123"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"subject": "Saatekiri on saadetud laborisse. Palun mine proove andma.",
|
||||
"heading": "Täname tellimuse eest!",
|
||||
"previewText": "Saatekiri uuringute tegemiseks on saadetud laborisse.",
|
||||
"p1": "Saatekiri uuringute tegemiseks on saadetud laborisse digitaalselt. Palun mine proove andma: {{partnerLocation}}.",
|
||||
"p2": "Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href='https://medreport.ee/et/verevotupunktid'>vaata asukohti ja lahtiolekuaegasid</a>.",
|
||||
"p3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
||||
"p4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>",
|
||||
"p5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.",
|
||||
"p6": "SYNLAB klienditoe telefon: 17123",
|
||||
"p1Urine": "Analüüsides on ette nähtud <strong>uriinianalüüs</strong>. Uriinianalüüsiks võta hommikune esmane uriin.",
|
||||
"p2Urine": "Proovitopsi võib soetada apteegist ja analüüsi kaasa võtta (teostada protseduur kodus) või küsida proovivõtupunktist (teostada protseduur proovipunkti wc-s)."
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"subject": "Saabusid tellitud uuringute esimesed tulemused",
|
||||
"previewText": "Esimesed uuringute tulemused on saabunud.",
|
||||
"p1": "Esimesed uuringute tulemused on saabunud:",
|
||||
"p2": "Saadame järgmise teavituse, kui kõik uuringute vastused on saabunud süsteemi.",
|
||||
"p3": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.",
|
||||
"p4": "SYNLAB klienditoe telefon: 17123"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"subject": "Kõikide tellitud uuringute tulemused on saabunud. Ootab arsti kokkuvõtet.",
|
||||
"previewText": "Kõikide uuringute tulemused on saabunud.",
|
||||
"p1": "Kõikide uuringute tulemused on saabunud:",
|
||||
"p2": "Saadame järgmise teavituse kui arsti kokkuvõte on koostatud.",
|
||||
"p3": "SYNLAB klienditoe telefon: 17123"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"subject": "Получено заключение врача по заказу {{orderNr}}",
|
||||
"previewText": "Врач отправил заключение по вашим результатам анализа.",
|
||||
"hello": "Здравствуйте, {{displayName}}",
|
||||
"summaryReceivedForOrder": "Врач подготовил пояснительное заключение по заказанным вами анализам.",
|
||||
"linkText": "Посмотреть заключение",
|
||||
"ifButtonDisabled": "Если кнопка не работает, скопируйте эту ссылку в ваш браузер:"
|
||||
"subject": "Заключение врача готово",
|
||||
"previewText": "Врач подготовил заключение по результатам анализов.",
|
||||
"p1": "Заключение врача готово:",
|
||||
"p2": "Рекомендуется проходить комплексное обследование регулярно, но как минимум один раз в год, если вы хотите сохранить активный и полноценный образ жизни.",
|
||||
"p3": "MedReport позволяет легко, удобно и быстро просматривать медицинские данные в одном месте и заказывать обследования.",
|
||||
"p4": "Телефон службы поддержки SYNLAB: 17123"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"subject": "Направление отправлено в лабораторию. Пожалуйста, сдайте анализы.",
|
||||
"heading": "Спасибо за заказ!",
|
||||
"previewText": "Направление на обследование отправлено в лабораторию.",
|
||||
"p1": "Направление на обследование было отправлено в лабораторию в цифровом виде. Пожалуйста, сдайте анализы: {{partnerLocation}}.",
|
||||
"p2": "Если у вас нет возможности прийти в выбранный пункт сдачи анализов, вы можете обратиться в любой удобный для вас пункт – <a href='https://medreport.ee/et/verevotupunktid'>посмотреть адреса и часы работы</a>.",
|
||||
"p3": "Рекомендуется сдавать анализы утром (до 12:00) натощак, без еды и напитков (разрешается пить воду).",
|
||||
"p4": "В пункте сдачи анализов выберите в системе очереди: в разделе <strong>направления</strong> → <strong>направление от специалиста</strong>.",
|
||||
"p5": "Если у вас возникли дополнительные вопросы, пожалуйста, свяжитесь с нами.",
|
||||
"p6": "Телефон службы поддержки SYNLAB: 17123",
|
||||
"p1Urine": "В обследование входит <strong>анализ мочи</strong>. Для анализа необходимо собрать первую утреннюю мочу.",
|
||||
"p2Urine": "Контейнер можно приобрести в аптеке и принести образец с собой (процедура проводится дома) или взять контейнер в пункте сдачи (процедура проводится в туалете пункта)."
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"subject": "Поступили первые результаты заказанных исследований",
|
||||
"previewText": "Первые результаты исследований поступили.",
|
||||
"p1": "Первые результаты исследований поступили:",
|
||||
"p2": "Мы отправим следующее уведомление, когда все результаты исследований будут получены в системе.",
|
||||
"p3": "Если у вас возникнут дополнительные вопросы, пожалуйста, свяжитесь с нами.",
|
||||
"p4": "Телефон службы поддержки SYNLAB: 17123"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"subject": "Все заказанные результаты исследований поступили. Ожидается заключение врача.",
|
||||
"previewText": "Все результаты исследований поступили.",
|
||||
"p1": "Все результаты исследований поступили:",
|
||||
"p2": "Мы отправим следующее уведомление, когда заключение врача будет подготовлено.",
|
||||
"p3": "Телефон службы поддержки SYNLAB: 17123"
|
||||
}
|
||||
@@ -126,7 +126,7 @@ export const giveFeedbackAction = doctorAction(
|
||||
|
||||
if (isCompleted) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED,
|
||||
action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: analysisOrderId,
|
||||
});
|
||||
@@ -136,7 +136,7 @@ export const giveFeedbackAction = doctorAction(
|
||||
} catch (e: any) {
|
||||
if (isCompleted) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED,
|
||||
action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED,
|
||||
status: 'FAIL',
|
||||
comment: e?.message,
|
||||
relatedRecordId: analysisOrderId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import z from 'zod/v3';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import z from 'zod';
|
||||
|
||||
export const doctorJobSelectSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
userId: z.uuid(),
|
||||
analysisOrderId: z.number(),
|
||||
});
|
||||
export type DoctorJobSelect = z.infer<typeof doctorJobSelectSchema>;
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"exports": {
|
||||
"./api": "./src/server/api.ts",
|
||||
"./components": "./src/components/index.ts",
|
||||
"./hooks": "./src/hooks/index.ts"
|
||||
"./hooks": "./src/hooks/index.ts",
|
||||
"./webhooks/*": "./src/server/services/webhooks/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import {
|
||||
renderAllResultsReceivedEmail,
|
||||
renderFirstResultsReceivedEmail,
|
||||
renderOrderProcessingEmail,
|
||||
renderPatientFirstResultsReceivedEmail,
|
||||
renderPatientFullResultsReceivedEmail,
|
||||
} from '@kit/email-templates';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getFullName } from '@kit/shared/utils';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
import {
|
||||
getAssignedDoctorAccount,
|
||||
getDoctorAccounts,
|
||||
getUserContactAdmin,
|
||||
} from '~/lib/services/account.service';
|
||||
import {
|
||||
NotificationAction,
|
||||
createNotificationLog,
|
||||
} from '~/lib/services/audit/notificationEntries.service';
|
||||
import {
|
||||
EmailRenderer,
|
||||
sendEmailFromTemplate,
|
||||
} from '~/lib/services/mailer.service';
|
||||
|
||||
type AnalysisOrder = Database['medreport']['Tables']['analysis_orders']['Row'];
|
||||
|
||||
export function createAnalysisOrderWebhooksService() {
|
||||
return new AnalysisOrderWebhooksService();
|
||||
}
|
||||
|
||||
class AnalysisOrderWebhooksService {
|
||||
private readonly namespace = 'analysis_orders.webhooks';
|
||||
|
||||
async handleStatusChangeWebhook(analysisOrder: AnalysisOrder) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
analysisOrderId: analysisOrder.id,
|
||||
namespace: this.namespace,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Received status change update. Processing...');
|
||||
let actions: NotificationAction[] = [];
|
||||
try {
|
||||
if (analysisOrder.status === 'PROCESSING') {
|
||||
actions = [NotificationAction.PATIENT_ORDER_PROCESSING];
|
||||
await this.sendProcessingNotification(analysisOrder);
|
||||
}
|
||||
|
||||
if (analysisOrder.status === 'PARTIAL_ANALYSIS_RESPONSE') {
|
||||
actions = [
|
||||
NotificationAction.PATIENT_FIRST_RESULTS_RECEIVED,
|
||||
NotificationAction.DOCTOR_NEW_JOBS,
|
||||
];
|
||||
|
||||
await this.sendPartialAnalysisResultsNotifications(analysisOrder);
|
||||
}
|
||||
|
||||
if (analysisOrder.status === 'FULL_ANALYSIS_RESPONSE') {
|
||||
actions = [
|
||||
NotificationAction.DOCTOR_PATIENT_RESULTS_RECEIVED,
|
||||
NotificationAction.PATIENT_FULL_RESULTS_RECEIVED,
|
||||
];
|
||||
await this.sendFullAnalysisResultsNotifications(analysisOrder);
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
return logger.info(ctx, 'Status change notifications sent.');
|
||||
}
|
||||
|
||||
logger.info(ctx, 'Status change processed. No notifications to send.');
|
||||
} catch (e: any) {
|
||||
if (actions.length)
|
||||
await Promise.all(
|
||||
actions.map((action) =>
|
||||
createNotificationLog({
|
||||
action,
|
||||
status: 'FAIL',
|
||||
comment: e?.message,
|
||||
relatedRecordId: analysisOrder.id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
logger.error(
|
||||
ctx,
|
||||
`Error while processing status change: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async sendProcessingNotification(analysisOrder: AnalysisOrder) {
|
||||
const logger = await getLogger();
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
|
||||
const userContact = await getUserContactAdmin(analysisOrder.user_id);
|
||||
|
||||
if (!userContact?.email) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.PATIENT_ORDER_PROCESSING,
|
||||
status: 'FAIL',
|
||||
comment: 'No email found for ' + analysisOrder.user_id,
|
||||
relatedRecordId: analysisOrder.id,
|
||||
});
|
||||
logger.warn(
|
||||
{ analysisOrderId: analysisOrder.id, namespace: this.namespace },
|
||||
'No email found ',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const [{ data: medusaOrder }, { data: analysisElements }] =
|
||||
await Promise.all([
|
||||
supabase
|
||||
.from('order')
|
||||
.select('id,metadata')
|
||||
.eq('id', analysisOrder.medusa_order_id)
|
||||
.single()
|
||||
.throwOnError(),
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.select('materialGroups:material_groups')
|
||||
.in('id', analysisOrder.analysis_element_ids ?? [])
|
||||
.throwOnError(),
|
||||
]);
|
||||
|
||||
let isUrine = false;
|
||||
for (const analysisElement of analysisElements ?? []) {
|
||||
logger.info({ group: analysisElement.materialGroups ?? [] });
|
||||
|
||||
const containsUrineSample = (analysisElement.materialGroups ?? [])?.some(
|
||||
(element) =>
|
||||
(element as { Materjal?: { MaterjaliNimi: string } })?.Materjal
|
||||
?.MaterjaliNimi === 'Uriin',
|
||||
);
|
||||
|
||||
if (containsUrineSample) {
|
||||
isUrine = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const orderMetadata = medusaOrder.metadata as {
|
||||
partner_location_name?: string;
|
||||
};
|
||||
|
||||
await sendEmailFromTemplate(
|
||||
renderOrderProcessingEmail,
|
||||
{
|
||||
language: userContact.preferred_locale ?? 'et',
|
||||
recipientName: getFullName(userContact.name, userContact.last_name),
|
||||
partnerLocation: orderMetadata.partner_location_name ?? 'SYNLAB',
|
||||
isUrine,
|
||||
},
|
||||
userContact.email,
|
||||
);
|
||||
|
||||
return createNotificationLog({
|
||||
action: NotificationAction.PATIENT_ORDER_PROCESSING,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: analysisOrder.id,
|
||||
});
|
||||
}
|
||||
|
||||
async sendPatientUpdateNotification(
|
||||
analysisOrder: AnalysisOrder,
|
||||
template: EmailRenderer,
|
||||
action: NotificationAction,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const userContact = await getUserContactAdmin(analysisOrder.user_id);
|
||||
|
||||
if (userContact?.email) {
|
||||
await sendEmailFromTemplate(
|
||||
template,
|
||||
{
|
||||
analysisOrderId: analysisOrder.id,
|
||||
recipientName: getFullName(userContact.name, userContact.last_name),
|
||||
language: userContact.preferred_locale ?? 'et',
|
||||
},
|
||||
userContact.email,
|
||||
);
|
||||
await createNotificationLog({
|
||||
action,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: analysisOrder.id,
|
||||
});
|
||||
logger.info(
|
||||
{ analysisOrderId: analysisOrder.id, namespace: this.namespace },
|
||||
'Sent notification email',
|
||||
);
|
||||
} else {
|
||||
await createNotificationLog({
|
||||
action,
|
||||
status: 'FAIL',
|
||||
comment: 'No email found for ' + analysisOrder.user_id,
|
||||
relatedRecordId: analysisOrder.id,
|
||||
});
|
||||
logger.warn(
|
||||
{ analysisOrderId: analysisOrder.id, namespace: this.namespace },
|
||||
'No email found ',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPartialAnalysisResultsNotifications(analysisOrder: AnalysisOrder) {
|
||||
const logger = await getLogger();
|
||||
|
||||
await this.sendPatientUpdateNotification(
|
||||
analysisOrder,
|
||||
renderPatientFirstResultsReceivedEmail,
|
||||
NotificationAction.PATIENT_FIRST_RESULTS_RECEIVED,
|
||||
);
|
||||
|
||||
const doctorAccounts = await getDoctorAccounts();
|
||||
const doctorEmails: string[] = doctorAccounts
|
||||
.map(({ email }) => email)
|
||||
.filter((email): email is string => !!email);
|
||||
|
||||
await sendEmailFromTemplate(
|
||||
renderFirstResultsReceivedEmail,
|
||||
{
|
||||
analysisOrderId: analysisOrder.id,
|
||||
language: 'et',
|
||||
},
|
||||
doctorEmails,
|
||||
);
|
||||
|
||||
logger.info(
|
||||
{ analysisOrderId: analysisOrder.id, namespace: this.namespace },
|
||||
'Sent out partial analysis results notifications for doctors',
|
||||
);
|
||||
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_NEW_JOBS,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: analysisOrder.id,
|
||||
});
|
||||
}
|
||||
|
||||
async sendFullAnalysisResultsNotifications(analysisOrder: AnalysisOrder) {
|
||||
await this.sendPatientUpdateNotification(
|
||||
analysisOrder,
|
||||
renderPatientFullResultsReceivedEmail,
|
||||
NotificationAction.PATIENT_FULL_RESULTS_RECEIVED,
|
||||
);
|
||||
|
||||
const doctorAccount = await getAssignedDoctorAccount(analysisOrder.id);
|
||||
const assignedDoctorEmail = doctorAccount?.email;
|
||||
|
||||
if (!assignedDoctorEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendEmailFromTemplate(
|
||||
renderAllResultsReceivedEmail,
|
||||
{
|
||||
analysisOrderId: analysisOrder.id,
|
||||
language: 'et',
|
||||
},
|
||||
assignedDoctorEmail,
|
||||
);
|
||||
|
||||
return createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_PATIENT_RESULTS_RECEIVED,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: analysisOrder.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user