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:
@@ -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);
|
||||
|
||||
23
app/api/job/handler/send-open-jobs-emails.ts
Normal file
23
app/api/job/handler/send-open-jobs-emails.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
53
app/api/job/send-open-jobs-emails/route.ts
Normal file
53
app/api/job/send-open-jobs-emails/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
@@ -41,3 +41,43 @@ export async function getAccountAdmin({
|
||||
|
||||
return data as unknown as AccountWithMemberships;
|
||||
}
|
||||
|
||||
export async function getDoctorAccounts() {
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('id, email, name, last_name, preferred_locale')
|
||||
.eq('is_personal_account', true)
|
||||
.eq('application_role', 'doctor')
|
||||
.throwOnError();
|
||||
|
||||
return data?.map(({ id, email, name, last_name, preferred_locale }) => ({
|
||||
id,
|
||||
email,
|
||||
name,
|
||||
lastName: last_name,
|
||||
preferredLocale: preferred_locale,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getAssignedDoctorAccount(analysisOrderId: number) {
|
||||
const { data: doctorUser } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('doctor_user_id')
|
||||
.eq('analysis_order_id', analysisOrderId)
|
||||
.throwOnError();
|
||||
|
||||
const doctorData = doctorUser[0];
|
||||
if (!doctorData || !doctorData.doctor_user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('email')
|
||||
.eq('primary_owner_user_id', doctorData.doctor_user_id);
|
||||
|
||||
return { email: data?.[0]?.email };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
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',
|
||||
}
|
||||
|
||||
export const createNotificationLog = async ({
|
||||
@@ -17,7 +19,7 @@ export const createNotificationLog = async ({
|
||||
relatedRecordId?: string | number;
|
||||
}) => {
|
||||
try {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
|
||||
await supabase
|
||||
.schema('audit')
|
||||
@@ -30,6 +32,6 @@ export const createNotificationLog = async ({
|
||||
})
|
||||
.throwOnError();
|
||||
} catch (error) {
|
||||
console.error('Failed to insert doctor page view log', error);
|
||||
console.error('Failed to insert doctor notification log', error);
|
||||
}
|
||||
};
|
||||
|
||||
32
lib/services/doctor-jobs.service.ts
Normal file
32
lib/services/doctor-jobs.service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
async function getAssignedOrderIds() {
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
|
||||
const { data: assignedOrderIds } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('analysis_order_id')
|
||||
.not('doctor_user_id', 'is', null)
|
||||
.throwOnError();
|
||||
|
||||
return assignedOrderIds?.map((f) => f.analysis_order_id) || [];
|
||||
}
|
||||
|
||||
export async function getOpenJobAnalysisResponseIds() {
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
const assignedIds = await getAssignedOrderIds();
|
||||
|
||||
let query = supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select('id, analysis_order_id')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (assignedIds.length > 0) {
|
||||
query = query.not('analysis_order_id', 'in', `(${assignedIds.join(',')})`);
|
||||
}
|
||||
|
||||
const { data: analysisResponses } = await query.throwOnError();
|
||||
return analysisResponses?.map(({ id }) => id) || [];
|
||||
}
|
||||
@@ -1,50 +1,41 @@
|
||||
'use server';
|
||||
|
||||
import { CompanySubmitData } from '@/lib/types/company';
|
||||
import { emailSchema } from '@/lib/validations/email.schema';
|
||||
import { toArray } from '@/lib/utils';
|
||||
|
||||
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
||||
import { getMailer } from '@kit/mailers';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
export const sendDoctorSummaryCompletedEmail = async (
|
||||
language: string,
|
||||
recipientName: string,
|
||||
recipientEmail: string,
|
||||
orderNr: string,
|
||||
orderId: number,
|
||||
) => {
|
||||
const { html, subject } = await renderDoctorSummaryReceivedEmail({
|
||||
language,
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
orderNr,
|
||||
orderId,
|
||||
});
|
||||
import { emailSchema } from '~/lib/validations/email.schema';
|
||||
|
||||
await sendEmail({
|
||||
subject,
|
||||
html,
|
||||
to: recipientEmail,
|
||||
});
|
||||
type EmailTemplate = {
|
||||
html: string;
|
||||
subject: string;
|
||||
};
|
||||
|
||||
export const sendCompanyOfferEmail = async (
|
||||
data: CompanySubmitData,
|
||||
language: string,
|
||||
) => {
|
||||
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
|
||||
const { html, subject } = await renderCompanyOfferEmail({
|
||||
language,
|
||||
companyData: data,
|
||||
});
|
||||
type EmailRenderer<T = any> = (params: T) => Promise<EmailTemplate>;
|
||||
|
||||
await sendEmail({
|
||||
export const sendEmailFromTemplate = async <T>(
|
||||
renderer: EmailRenderer<T>,
|
||||
templateParams: T,
|
||||
recipients: string | string[],
|
||||
) => {
|
||||
const { html, subject } = await renderer(templateParams);
|
||||
|
||||
const recipientsArray = toArray(recipients);
|
||||
if (!recipientsArray.length) {
|
||||
throw new Error('No valid email recipients provided');
|
||||
}
|
||||
|
||||
const emailPromises = recipientsArray.map((email) =>
|
||||
sendEmail({
|
||||
subject,
|
||||
html,
|
||||
to: process.env.CONTACT_EMAIL || '',
|
||||
});
|
||||
to: email,
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(emailPromises);
|
||||
};
|
||||
|
||||
export const sendEmail = enhanceAction(
|
||||
@@ -53,7 +44,7 @@ export const sendEmail = enhanceAction(
|
||||
const log = await getLogger();
|
||||
|
||||
if (!process.env.EMAIL_USER) {
|
||||
log.error('Sending email failed, as no sender found in env.')
|
||||
log.error('Sending email failed, as no sender was found in env.');
|
||||
throw new Error('No email user configured');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
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(
|
||||
@@ -42,6 +55,12 @@ class DatabaseWebhookRouterService {
|
||||
return this.handleAccountsWebhook(payload);
|
||||
}
|
||||
|
||||
case 'analysis_orders': {
|
||||
const payload = body as RecordChange<typeof body.table>;
|
||||
|
||||
return this.handleAnalysisOrdersWebhook(payload);
|
||||
}
|
||||
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
@@ -83,4 +102,69 @@ class DatabaseWebhookRouterService {
|
||||
return service.handleAccountDeletedWebhook(body.old_record);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAnalysisOrdersWebhook(
|
||||
body: RecordChange<'analysis_orders'>,
|
||||
) {
|
||||
if (body.type === 'UPDATE' && body.record && body.old_record) {
|
||||
const { record, old_record } = body;
|
||||
|
||||
if (record.status === old_record.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
let action;
|
||||
try {
|
||||
const data = {
|
||||
analysisOrderId: record.id,
|
||||
language: 'et',
|
||||
};
|
||||
|
||||
if (record.status === 'PARTIAL_ANALYSIS_RESPONSE') {
|
||||
action = NotificationAction.NEW_JOBS_ALERT;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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 { EmailButton } from '../components/email-button';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
export async function renderAllResultsReceivedEmail({
|
||||
language,
|
||||
analysisOrderId,
|
||||
}: {
|
||||
language: string;
|
||||
analysisOrderId: number;
|
||||
}) {
|
||||
const namespace = 'all-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>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`)}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] font-semibold text-[#242424]">
|
||||
{t(`${namespace}:openOrdersHeading`)}
|
||||
</Text>
|
||||
<EmailButton
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
|
||||
>
|
||||
{t(`${namespace}:linkText`)}
|
||||
</EmailButton>
|
||||
|
||||
<Text>
|
||||
{t(`${namespace}:ifLinksDisabled`)}{' '}
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
|
||||
</Text>
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
@@ -19,14 +19,12 @@ import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
export async function renderDoctorSummaryReceivedEmail({
|
||||
language,
|
||||
recipientEmail,
|
||||
recipientName,
|
||||
orderNr,
|
||||
orderId,
|
||||
}: {
|
||||
language?: string;
|
||||
recipientName: string;
|
||||
recipientEmail: string;
|
||||
orderNr: string;
|
||||
orderId: number;
|
||||
}) {
|
||||
@@ -37,8 +35,6 @@ export async function renderDoctorSummaryReceivedEmail({
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const to = recipientEmail;
|
||||
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
orderNr,
|
||||
});
|
||||
@@ -92,6 +88,5 @@ export async function renderDoctorSummaryReceivedEmail({
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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 { EmailButton } from '../components/email-button';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
export async function renderFirstResultsReceivedEmail({
|
||||
language,
|
||||
analysisOrderId,
|
||||
}: {
|
||||
language: string;
|
||||
analysisOrderId: number;
|
||||
}) {
|
||||
const namespace = '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>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`)}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:resultsReceivedForOrders`)}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] font-semibold text-[#242424]">
|
||||
{t(`${namespace}:openOrdersHeading`)}
|
||||
</Text>
|
||||
|
||||
<EmailButton
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
|
||||
>
|
||||
{t(`${namespace}:linkText`)}
|
||||
</EmailButton>
|
||||
|
||||
<Text>
|
||||
{t(`${namespace}:ifLinksDisabled`)}{' '}
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
|
||||
</Text>
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
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 renderNewJobsAvailableEmail({
|
||||
language,
|
||||
analysisResponseIds,
|
||||
}: {
|
||||
language?: string;
|
||||
analysisResponseIds: number[];
|
||||
}) {
|
||||
const namespace = 'new-jobs-available-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language,
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
nr: analysisResponseIds.length,
|
||||
});
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
nr: analysisResponseIds.length,
|
||||
});
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`)}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:resultsReceivedForOrders`, {
|
||||
nr: analysisResponseIds.length,
|
||||
})}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] font-semibold text-[#242424]">
|
||||
{t(`${namespace}:openOrdersHeading`, {
|
||||
nr: analysisResponseIds.length,
|
||||
})}
|
||||
</Text>
|
||||
<ul className="list-none text-[16px] leading-[24px]">
|
||||
{analysisResponseIds.map((analysisResponseId, index) => (
|
||||
<li>
|
||||
<Link
|
||||
key={analysisResponseId}
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisResponseId}`}
|
||||
>
|
||||
{t(`${namespace}:linkText`, { nr: index + 1 })}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Text>
|
||||
{t(`${namespace}:ifLinksDisabled`)}{' '}
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/open-jobs`}
|
||||
</Text>
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export async function renderOtpEmail(props: Props) {
|
||||
|
||||
<Section className="mb-[16px] mt-[16px] text-center">
|
||||
<Button className={'w-full rounded bg-neutral-950 text-center'}>
|
||||
<Text className="text-[16px] font-medium font-semibold leading-[16px] text-white">
|
||||
<Text className="text-[16px] font-semibold leading-[16px] text-white">
|
||||
{props.otp}
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
} 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';
|
||||
import CommonFooter from '../components/common-footer';
|
||||
|
||||
interface Props {
|
||||
analysisPackageName: string;
|
||||
@@ -31,7 +31,10 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) {
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`);
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
analysisPackageName: props.analysisPackageName,
|
||||
});
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
analysisPackageName: props.analysisPackageName,
|
||||
});
|
||||
|
||||
@@ -4,3 +4,6 @@ export * from './emails/otp.email';
|
||||
export * from './emails/company-offer.email';
|
||||
export * from './emails/synlab.email';
|
||||
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';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previewText": "All analysis results have been received",
|
||||
"subject": "All patient analysis results have been received",
|
||||
"openOrdersHeading": "Review the results and prepare a summary:",
|
||||
"linkText": "See results",
|
||||
"ifLinksDisabled": "If the link does not work, you can see the results by copying this link into your browser.",
|
||||
"hello": "Hello"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"previewText": "First analysis responses received",
|
||||
"subject": "New job - first analysis responses received",
|
||||
"resultsReceivedForOrders": "New job available to claim",
|
||||
"openOrdersHeading": "See here:",
|
||||
"linkText": "See results",
|
||||
"ifLinksDisabled": "If the link does not work, you can see available jobs by copying this link into your browser.",
|
||||
"hello": "Hello,"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"previewText": "New jobs available",
|
||||
"subject": "Please write a summary",
|
||||
"resultsReceivedForOrders": "Please review the results and write a summary.",
|
||||
"openOrdersHeading": "See here:",
|
||||
"linkText": "Open job {{nr}}",
|
||||
"ifLinksDisabled": "If the links do not work, you can see available jobs by copying this link into your browser.",
|
||||
"hello": "Hello,"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previewText": "Kõik analüüside vastused on saabunud",
|
||||
"subject": "Patsiendi kõikide analüüside vastused on saabunud",
|
||||
"openOrdersHeading": "Vaata tulemusi ja kirjuta kokkuvõte:",
|
||||
"linkText": "Vaata tulemusi",
|
||||
"ifLinksDisabled": "Kui link ei tööta, näed analüüsitulemusi sellelt aadressilt:",
|
||||
"hello": "Tere"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"previewText": "Saabusid esimesed analüüside vastused",
|
||||
"subject": "Uus töö - saabusid esimesed analüüside vastused",
|
||||
"resultsReceivedForOrders": "Patsiendile saabusid esimesed analüüside vastused.",
|
||||
"openOrdersHeading": "Vaata siit:",
|
||||
"linkText": "Vaata tulemusi",
|
||||
"ifLinksDisabled": "Kui link ei tööta, näed analüüsitulemusi sellelt aadressilt:",
|
||||
"hello": "Tere"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"previewText": "Palun koosta kokkuvõte",
|
||||
"subject": "Palun koosta kokkuvõte",
|
||||
"resultsReceivedForOrders": "Palun vaata tulemused üle ja kirjuta kokkuvõte.",
|
||||
"openOrdersHeading": "Vaata siit:",
|
||||
"linkText": "Töö {{nr}}",
|
||||
"ifLinksDisabled": "Kui lingid ei tööta, näed vabasid töid sellelt aadressilt:",
|
||||
"hello": "Tere"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previewText": "All analysis results have been received",
|
||||
"subject": "All patient analysis results have been received",
|
||||
"openOrdersHeading": "Review the results and prepare a summary:",
|
||||
"linkText": "See results",
|
||||
"ifLinksDisabled": "If the link does not work, you can see the results by copying this link into your browser.",
|
||||
"hello": "Hello"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"previewText": "First analysis responses received",
|
||||
"subject": "New job - first analysis responses received",
|
||||
"resultsReceivedForOrders": "New job available to claim",
|
||||
"openOrdersHeading": "See here:",
|
||||
"linkText": "See results",
|
||||
"ifLinksDisabled": "If the link does not work, you can see the results by copying this link into your browser.",
|
||||
"hello": "Hello,"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"previewText": "New jobs available",
|
||||
"subject": "Please write a summary",
|
||||
"resultsReceivedForOrders": "Please review the results and write a summary.",
|
||||
"openOrdersHeading": "See here:",
|
||||
"linkText": "Open job {{nr}}",
|
||||
"ifLinksDisabled": "If the links do not work, you can see available jobs by copying this link into your browser.",
|
||||
"hello": "Hello,"
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export const PatientSchema = z.object({
|
||||
email: z.string().nullable(),
|
||||
height: z.number().optional().nullable(),
|
||||
weight: z.number().optional().nullable(),
|
||||
preferred_locale: z.string().nullable(),
|
||||
});
|
||||
export type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ export const AccountSchema = z.object({
|
||||
last_name: z.string().nullable(),
|
||||
id: z.string(),
|
||||
primary_owner_user_id: z.string(),
|
||||
preferred_locale: z.string().nullable(),
|
||||
});
|
||||
export type Account = z.infer<typeof AccountSchema>;
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ import 'server-only';
|
||||
|
||||
import { isBefore } from 'date-fns';
|
||||
|
||||
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
||||
import { getFullName } from '@kit/shared/utils';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { sendDoctorSummaryCompletedEmail } from '../../../../../../../lib/services/mailer.service';
|
||||
import { sendEmailFromTemplate } from '../../../../../../../lib/services/mailer.service';
|
||||
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
|
||||
import {
|
||||
AnalysisResponseBase,
|
||||
@@ -54,7 +55,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('name, last_name, id, primary_owner_user_id')
|
||||
.select('name, last_name, id, primary_owner_user_id, preferred_locale')
|
||||
.in('primary_owner_user_id', userIds),
|
||||
]);
|
||||
|
||||
@@ -67,7 +68,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
||||
? await supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('name, last_name, id, primary_owner_user_id')
|
||||
.select('name, last_name, id, primary_owner_user_id, preferred_locale')
|
||||
.in('primary_owner_user_id', doctorUserIds)
|
||||
: { data: [] };
|
||||
|
||||
@@ -408,7 +409,7 @@ export async function getAnalysisResultsForDoctor(
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select(
|
||||
`primary_owner_user_id, id, name, last_name, personal_code, phone, email,
|
||||
`primary_owner_user_id, id, name, last_name, personal_code, phone, email, preferred_locale,
|
||||
account_params(height,weight)`,
|
||||
)
|
||||
.eq('is_personal_account', true)
|
||||
@@ -472,6 +473,7 @@ export async function getAnalysisResultsForDoctor(
|
||||
personal_code,
|
||||
phone,
|
||||
account_params,
|
||||
preferred_locale,
|
||||
} = accountWithParams[0];
|
||||
|
||||
const analysisResponseElementsWithPreviousData = [];
|
||||
@@ -503,6 +505,7 @@ export async function getAnalysisResultsForDoctor(
|
||||
},
|
||||
doctorFeedback: doctorFeedback?.[0],
|
||||
patient: {
|
||||
preferred_locale,
|
||||
userId: primary_owner_user_id,
|
||||
accountId,
|
||||
firstName: name,
|
||||
@@ -638,7 +641,7 @@ export async function submitFeedback(
|
||||
}
|
||||
|
||||
if (status === 'COMPLETED') {
|
||||
const [{ data: recipient }, { data: medusaOrderIds }] = await Promise.all([
|
||||
const [{ data: recipient }, { data: analysisOrder }] = await Promise.all([
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
@@ -659,18 +662,21 @@ export async function submitFeedback(
|
||||
throw new Error('Could not find user email.');
|
||||
}
|
||||
|
||||
if (!medusaOrderIds?.[0]?.id) {
|
||||
if (!analysisOrder?.[0]?.id) {
|
||||
throw new Error('Could not retrieve order.');
|
||||
}
|
||||
|
||||
const { preferred_locale, name, last_name, email } = recipient[0];
|
||||
|
||||
await sendDoctorSummaryCompletedEmail(
|
||||
preferred_locale ?? 'et',
|
||||
getFullName(name, last_name),
|
||||
await sendEmailFromTemplate(
|
||||
renderDoctorSummaryReceivedEmail,
|
||||
{
|
||||
language: preferred_locale ?? 'et',
|
||||
recipientName: getFullName(name, last_name),
|
||||
orderNr: analysisOrder?.[0]?.medusa_order_id ?? '',
|
||||
orderId: analysisOrder[0].id,
|
||||
},
|
||||
email,
|
||||
medusaOrderIds?.[0]?.medusa_order_id ?? '',
|
||||
medusaOrderIds[0].id,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './use-csrf-token';
|
||||
export * from './use-current-locale-language-names';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function useLanguageName(currentLanguage: string) {
|
||||
return useMemo(() => {
|
||||
return new Intl.DisplayNames([currentLanguage], {
|
||||
type: 'language',
|
||||
});
|
||||
}, [currentLanguage]);
|
||||
}
|
||||
|
||||
export function useCurrentLocaleLanguageNames() {
|
||||
const { i18n } = useTranslation();
|
||||
const { language: currentLanguage } = i18n;
|
||||
return useLanguageName(currentLanguage);
|
||||
}
|
||||
@@ -116,5 +116,6 @@
|
||||
"confirm": "Confirm",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"invalidDataError": "Invalid data submitted"
|
||||
"invalidDataError": "Invalid data submitted",
|
||||
"language": "Language"
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"assignedTo": "Doctor",
|
||||
"resultsStatus": "Analysis results",
|
||||
"waitingForNr": "Waiting for {{nr}}",
|
||||
"language": "Preferred language",
|
||||
"responsesReceived": "Results complete"
|
||||
},
|
||||
"otherPatients": "Other patients",
|
||||
|
||||
@@ -136,5 +136,6 @@
|
||||
"confirm": "Kinnita",
|
||||
"previous": "Eelmine",
|
||||
"next": "Järgmine",
|
||||
"invalidDataError": "Vigased andmed"
|
||||
"invalidDataError": "Vigased andmed",
|
||||
"language": "Keel"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"assignedTo": "Arst",
|
||||
"resultsStatus": "Analüüsitulemused",
|
||||
"waitingForNr": "Ootel {{nr}}",
|
||||
"language": "Patsiendi keel",
|
||||
"responsesReceived": "Tulemused koos"
|
||||
},
|
||||
"otherPatients": "Muud patsiendid",
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
"patientName": "Имя пациента",
|
||||
"serviceName": "Услуга",
|
||||
"orderNr": "Номер заказа",
|
||||
"time": "Сдача пробы",
|
||||
"time": "Время",
|
||||
"assignedTo": "Врач",
|
||||
"resultsStatus": "Результаты анализов",
|
||||
"waitingForNr": "В ожидании {{nr}}",
|
||||
"responsesReceived": "Результаты получены"
|
||||
"language": "Предпочтительный язык",
|
||||
"responsesReceived": "Результаты готовы"
|
||||
},
|
||||
"otherPatients": "Другие пациенты",
|
||||
"analyses": "Анализы",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
create extension if not exists pg_cron;
|
||||
create extension if not exists pg_net;
|
||||
|
||||
select
|
||||
cron.schedule(
|
||||
'send emails with new unassigned jobs 4x a day',
|
||||
'0 4,9,14,18 * * 1-5', -- Run at 07:00, 12:00, 17:00 and 21:00 (GMT +3) on weekdays only
|
||||
$$
|
||||
select
|
||||
net.http_post(
|
||||
url := 'https://test.medreport.ee/api/job/send-open-jobs-emails',
|
||||
headers := jsonb_build_object(
|
||||
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
|
||||
)
|
||||
) as request_id;
|
||||
$$
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
grant select on table "medreport"."doctor_analysis_feedback" to "service_role";
|
||||
|
||||
create policy "service_role_select"
|
||||
on "medreport"."doctor_analysis_feedback"
|
||||
as permissive
|
||||
for select
|
||||
to service_role
|
||||
using (true);
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
create trigger "trigger_doctor_notification" after update
|
||||
on "medreport"."analysis_orders" for each row
|
||||
execute function "supabase_functions"."http_request"(
|
||||
'http://host.docker.internal:3000/api/db/webhook',
|
||||
'POST',
|
||||
'{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}',
|
||||
'{}',
|
||||
'5000'
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
alter table audit.notification_entries enable row level security;
|
||||
|
||||
create policy "service_role_insert"
|
||||
on "audit"."notification_entries"
|
||||
as permissive
|
||||
for insert
|
||||
to service_role
|
||||
with check (true);
|
||||
|
||||
grant insert on table "audit"."notification_entries" to "service_role";
|
||||
Reference in New Issue
Block a user