MED-88: add doctor email notifications (#65)

* MED-88: add doctor email notifications

* add logging, send open jobs notification on partial analysis response

* update permissions

* fix import, permissions

* casing, let email be null

* unused import
This commit is contained in:
Helena
2025-09-02 12:14:01 +03:00
committed by GitHub
parent 56a832b96b
commit 3498406a0c
41 changed files with 751 additions and 69 deletions

View File

@@ -4,14 +4,15 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { SubmitButton } from '@kit/shared/components/ui/submit-button';
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { sendEmailFromTemplate } from '@/lib/services/mailer.service';
import { CompanySubmitData } from '@/lib/types/company';
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { renderCompanyOfferEmail } from '@kit/email-templates';
import { SubmitButton } from '@kit/shared/components/ui/submit-button';
import { FormItem } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
@@ -39,7 +40,14 @@ const CompanyOfferForm = () => {
});
try {
sendCompanyOfferEmail(data, language)
sendEmailFromTemplate(
renderCompanyOfferEmail,
{
companyData: data,
language,
},
process.env.CONTACT_EMAIL!,
)
.then(() => router.push('/company-offer/success'))
.catch((error) => {
setIsLoading(false);

View File

@@ -0,0 +1,23 @@
import { renderNewJobsAvailableEmail } from '@kit/email-templates';
import { getDoctorAccounts } from '~/lib/services/account.service';
import { getOpenJobAnalysisResponseIds } from '~/lib/services/doctor-jobs.service';
import { sendEmailFromTemplate } from '~/lib/services/mailer.service';
export default async function sendOpenJobsEmails() {
const analysisResponseIds = await getOpenJobAnalysisResponseIds();
const doctorAccounts = await getDoctorAccounts();
const doctorEmails: string[] = doctorAccounts
.map(({ email }) => email)
.filter((email): email is string => !!email);
await sendEmailFromTemplate(
renderNewJobsAvailableEmail,
{
language: 'et',
analysisResponseIds,
},
doctorEmails,
);
}

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import {
NotificationAction,
createNotificationLog,
} from '~/lib/services/audit/notificationEntries.service';
import loadEnv from '../handler/load-env';
import sendOpenJobsEmails from '../handler/send-open-jobs-emails';
import validateApiKey from '../handler/validate-api-key';
export const POST = async (request: NextRequest) => {
loadEnv();
try {
validateApiKey(request);
} catch (e) {
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
}
try {
await sendOpenJobsEmails();
console.info(
'Successfully sent out open job notification emails to doctors.',
);
await createNotificationLog({
action: NotificationAction.NEW_JOBS_ALERT,
status: 'SUCCESS',
});
return NextResponse.json(
{
message:
'Successfully sent out open job notification emails to doctors.',
},
{ status: 200 },
);
} catch (e: any) {
console.error(
'Error sending out open job notification emails to doctors.',
e,
);
await createNotificationLog({
action: NotificationAction.NEW_JOBS_ALERT,
status: 'FAIL',
comment: e?.message,
});
return NextResponse.json(
{
message: 'Failed to send out open job notification emails to doctors.',
},
{ status: 500 },
);
}
};

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQueryClient } from '@tanstack/react-query';
import { capitalize } from 'lodash';
import { useForm } from 'react-hook-form';
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
@@ -22,6 +23,9 @@ import {
doctorAnalysisFeedbackFormSchema,
} from '@kit/doctor/schema/doctor-analysis.schema';
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
import {
useCurrentLocaleLanguageNames
} from '@kit/shared/hooks';
import { getFullName } from '@kit/shared/utils';
import { useUser } from '@kit/supabase/hooks/use-user';
import { Button } from '@kit/ui/button';
@@ -57,6 +61,8 @@ export default function AnalysisView({
const { data: user } = useUser();
const languageNames = useCurrentLocaleLanguageNames();
const isInProgress = !!(
!!feedback?.status &&
feedback?.doctor_user_id &&
@@ -191,6 +197,12 @@ export default function AnalysisView({
<Trans i18nKey="doctor:email" />
</div>
<div>{patient.email}</div>
<div className="font-bold">
<Trans i18nKey="common:language" />
</div>
<div>
{capitalize(languageNames.of(patient.preferred_locale ?? 'et'))}
</div>
</div>
<div className="xs:hidden block">
<DoctorJobSelect

View File

@@ -10,6 +10,7 @@ import { Eye } from 'lucide-react';
import { getResultSetName } from '@kit/doctor/lib/helpers';
import { ResponseTable } from '@kit/doctor/schema/doctor-analysis.schema';
import { pathsConfig } from '@kit/shared/config';
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
import { getFullName } from '@kit/shared/utils';
import { useUser } from '@kit/supabase/hooks/use-user';
import { Button } from '@kit/ui/button';
@@ -23,7 +24,9 @@ import {
TableRow,
} from '@kit/ui/table';
import { Trans } from '@kit/ui/trans';
import DoctorJobSelect from './doctor-job-select';
import { capitalize } from 'lodash';
export default function ResultsTable({
results = [],
@@ -58,6 +61,8 @@ export default function ResultsTable({
const [isPending, startTransition] = useTransition();
const { data: currentUser } = useUser();
const languageNames = useCurrentLocaleLanguageNames();
const fetchPage = async (page: number) => {
startTransition(async () => {
const result = await fetchAction({
@@ -116,6 +121,9 @@ export default function ResultsTable({
<TableHead className="w-20">
<Trans i18nKey="doctor:resultsTable.resultsStatus" />
</TableHead>
<TableHead className="w-20">
<Trans i18nKey="doctor:resultsTable.language" />
</TableHead>
<TableHead className="w-20">
<Trans i18nKey="doctor:resultsTable.assignedTo" />
</TableHead>
@@ -179,6 +187,11 @@ export default function ResultsTable({
}}
/>
</TableCell>
<TableCell>
{capitalize(
languageNames.of(result?.patient?.preferred_locale ?? 'et'),
)}
</TableCell>
<TableCell>
<DoctorJobSelect
doctorUserId={result.doctor?.primary_owner_user_id}

View File

@@ -18,8 +18,8 @@ async function AnalysisPage({
id: string;
}>;
}) {
const { id: analysisResponseId } = await params;
const analysisResultDetails = await loadResult(Number(analysisResponseId));
const { id: analysisOrderId } = await params;
const analysisResultDetails = await loadResult(Number(analysisOrderId));
if (!analysisResultDetails) {
return null;
@@ -28,7 +28,7 @@ async function AnalysisPage({
if (analysisResultDetails) {
await createDoctorPageViewLog({
action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
recordKey: analysisResponseId,
recordKey: analysisOrderId,
dataOwnerUserId: analysisResultDetails.patient.userId,
});
}
@@ -50,3 +50,5 @@ async function AnalysisPage({
export default DoctorGuard(AnalysisPage);
const loadResult = cache(getAnalysisResultsForDoctor);