MED-145: send notification to patient when summary completed (#61)
* MED-145: send notification to patient when summary completed * MED-145: send notification to patient when summary completed * use aliased imports where possible, revert cart service urls * save language preference to local db * remove unnecessary optional chaning
This commit is contained in:
@@ -53,6 +53,7 @@ export default function AnalysisView({
|
||||
feedback?: DoctorFeedback;
|
||||
}) {
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
||||
|
||||
const { data: user } = useUser();
|
||||
|
||||
@@ -106,28 +107,22 @@ export default function AnalysisView({
|
||||
};
|
||||
|
||||
const handleDraftSubmit = async (e: React.FormEvent) => {
|
||||
setIsDraftSubmitting(true);
|
||||
e.preventDefault();
|
||||
|
||||
form.formState.errors.feedbackValue = undefined;
|
||||
const formData = form.getValues();
|
||||
onSubmit(formData, 'DRAFT');
|
||||
await onSubmit(formData, 'DRAFT');
|
||||
setIsDraftSubmitting(false);
|
||||
};
|
||||
|
||||
const handleCompleteSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleCompleteSubmit = form.handleSubmit(async () => {
|
||||
setIsConfirmOpen(true);
|
||||
};
|
||||
});
|
||||
|
||||
const confirmComplete = () => {
|
||||
const formData = form.getValues();
|
||||
onSubmit(formData, 'COMPLETED');
|
||||
};
|
||||
const confirmComplete = form.handleSubmit(async (data) => {
|
||||
await onSubmit(data, 'COMPLETED');
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -179,7 +174,11 @@ export default function AnalysisView({
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:bmi" />
|
||||
</div>
|
||||
<div>{bmiFromMetric(patient?.weight ?? 0, patient?.height ?? 0)}</div>
|
||||
<div>
|
||||
{patient?.weight && patient?.height
|
||||
? bmiFromMetric(patient.weight, patient.height)
|
||||
: '-'}
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:smoking" />
|
||||
</div>
|
||||
@@ -245,7 +244,9 @@ export default function AnalysisView({
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleDraftSubmit}
|
||||
disabled={isReadOnly}
|
||||
disabled={
|
||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||
}
|
||||
className="xs:w-1/4 w-full"
|
||||
>
|
||||
<Trans i18nKey="common:saveAsDraft" />
|
||||
@@ -253,7 +254,9 @@ export default function AnalysisView({
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCompleteSubmit}
|
||||
disabled={isReadOnly}
|
||||
disabled={
|
||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||
}
|
||||
className="xs:w-1/4 w-full"
|
||||
>
|
||||
<Trans i18nKey="common:save" />
|
||||
|
||||
35
lib/services/audit/notificationEntries.service.ts
Normal file
35
lib/services/audit/notificationEntries.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
export enum NotificationAction {
|
||||
DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED',
|
||||
}
|
||||
|
||||
export const createNotificationLog = async ({
|
||||
action,
|
||||
status,
|
||||
comment,
|
||||
relatedRecordId,
|
||||
}: {
|
||||
action: NotificationAction;
|
||||
status: Database['audit']['Enums']['action_status'];
|
||||
comment?: string;
|
||||
relatedRecordId?: string | number;
|
||||
}) => {
|
||||
try {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
await supabase
|
||||
.schema('audit')
|
||||
.from('notification_entries')
|
||||
.insert({
|
||||
action,
|
||||
status,
|
||||
comment,
|
||||
related_record_key: relatedRecordId?.toString(),
|
||||
})
|
||||
.throwOnError();
|
||||
} catch (error) {
|
||||
console.error('Failed to insert doctor page view log', error);
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,33 @@
|
||||
'use server';
|
||||
|
||||
import { CompanySubmitData } from '@/lib/types/company';
|
||||
import { emailSchema } from '@/lib/validations/email.schema';
|
||||
|
||||
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
||||
import { getMailer } from '@kit/mailers';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
|
||||
import { CompanySubmitData } from '../types/company';
|
||||
import { emailSchema } from '../validations/email.schema';
|
||||
export const sendDoctorSummaryCompletedEmail = async (
|
||||
language: string,
|
||||
recipientName: string,
|
||||
recipientEmail: string,
|
||||
orderNr: string,
|
||||
orderId: number,
|
||||
) => {
|
||||
const { html, subject } = await renderDoctorSummaryReceivedEmail({
|
||||
language,
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
orderNr,
|
||||
orderId,
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
subject,
|
||||
html,
|
||||
to: recipientEmail,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCompanyOfferEmail = async (
|
||||
data: CompanySubmitData,
|
||||
@@ -26,7 +49,6 @@ export const sendCompanyOfferEmail = async (
|
||||
export const sendEmail = enhanceAction(
|
||||
async ({ subject, html, to }) => {
|
||||
const mailer = await getMailer();
|
||||
|
||||
await mailer.sendEmail({
|
||||
to,
|
||||
subject,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
||||
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
||||
import { getCartId } from '@lib/data/cookies';
|
||||
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
||||
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { requireUserInServerComponent } from '../server/require-user-in-server-component';
|
||||
|
||||
const env = () => z
|
||||
const env = () =>
|
||||
z
|
||||
.object({
|
||||
medusaBackendPublicUrl: z
|
||||
.string({
|
||||
@@ -31,12 +34,12 @@ export async function handleAddToCart({
|
||||
selectedVariant,
|
||||
countryCode,
|
||||
}: {
|
||||
selectedVariant: Pick<StoreProductVariant, 'id'>
|
||||
countryCode: string
|
||||
selectedVariant: Pick<StoreProductVariant, 'id'>;
|
||||
countryCode: string;
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const user = await requireUserInServerComponent();
|
||||
const account = await loadCurrentUserAccount()
|
||||
const account = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
@@ -48,10 +51,7 @@ export async function handleAddToCart({
|
||||
countryCode,
|
||||
});
|
||||
|
||||
const { error } = await supabase
|
||||
.schema('audit')
|
||||
.from('cart_entries')
|
||||
.insert({
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
variant_id: selectedVariant.id,
|
||||
operation: 'ADD_TO_CART',
|
||||
account_id: account.id,
|
||||
@@ -65,25 +65,18 @@ export async function handleAddToCart({
|
||||
return cart;
|
||||
}
|
||||
|
||||
export async function handleDeleteCartItem({
|
||||
lineId,
|
||||
}: {
|
||||
lineId: string;
|
||||
}) {
|
||||
export async function handleDeleteCartItem({ lineId }: { lineId: string }) {
|
||||
await deleteLineItem(lineId);
|
||||
|
||||
const supabase = getSupabaseServerClient();
|
||||
const cartId = await getCartId();
|
||||
const user = await requireUserInServerComponent();
|
||||
const account = await loadCurrentUserAccount()
|
||||
const account = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.schema('audit')
|
||||
.from('cart_entries')
|
||||
.insert({
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
variant_id: lineId,
|
||||
operation: 'REMOVE_FROM_CART',
|
||||
account_id: account.id,
|
||||
@@ -95,20 +88,27 @@ export async function handleDeleteCartItem({
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleNavigateToPayment({ language, paymentSessionId }: { language: string, paymentSessionId: string }) {
|
||||
export async function handleNavigateToPayment({
|
||||
language,
|
||||
paymentSessionId,
|
||||
}: {
|
||||
language: string;
|
||||
paymentSessionId: string;
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const user = await requireUserInServerComponent();
|
||||
const account = await loadCurrentUserAccount()
|
||||
const account = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const cart = await retrieveCart();
|
||||
if (!cart) {
|
||||
throw new Error("No cart found");
|
||||
throw new Error('No cart found');
|
||||
}
|
||||
|
||||
const paymentLink = await new MontonioOrderHandlerService().getMontonioPaymentLink({
|
||||
const paymentLink =
|
||||
await new MontonioOrderHandlerService().getMontonioPaymentLink({
|
||||
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
|
||||
returnUrl: `${env().siteUrl}/home/cart/montonio-callback`,
|
||||
amount: cart.total,
|
||||
@@ -118,10 +118,7 @@ export async function handleNavigateToPayment({ language, paymentSessionId }: {
|
||||
merchantReference: `${account.id}:${paymentSessionId}:${cart.id}`,
|
||||
});
|
||||
|
||||
const { error } = await supabase
|
||||
.schema('audit')
|
||||
.from('cart_entries')
|
||||
.insert({
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
operation: 'NAVIGATE_TO_PAYMENT',
|
||||
account_id: account.id,
|
||||
cart_id: cart.id,
|
||||
@@ -137,21 +134,18 @@ export async function handleNavigateToPayment({ language, paymentSessionId }: {
|
||||
export async function handleLineItemTimeout({
|
||||
lineItem,
|
||||
}: {
|
||||
lineItem: StoreCartLineItem
|
||||
lineItem: StoreCartLineItem;
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const user = await requireUserInServerComponent();
|
||||
const account = await loadCurrentUserAccount()
|
||||
const account = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
await deleteLineItem(lineItem.id);
|
||||
|
||||
const { error } = await supabase
|
||||
.schema('audit')
|
||||
.from('cart_entries')
|
||||
.insert({
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
operation: 'LINE_ITEM_TIMEOUT',
|
||||
account_id: account.id,
|
||||
cart_id: lineItem.cart_id,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
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 renderDoctorSummaryReceivedEmail({
|
||||
language,
|
||||
recipientEmail,
|
||||
recipientName,
|
||||
orderNr,
|
||||
orderId,
|
||||
}: {
|
||||
language?: string;
|
||||
recipientName: string;
|
||||
recipientEmail: string;
|
||||
orderNr: string;
|
||||
orderId: number;
|
||||
}) {
|
||||
const namespace = 'doctor-summary-received-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language,
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const to = recipientEmail;
|
||||
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
orderNr,
|
||||
});
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
orderNr,
|
||||
});
|
||||
|
||||
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`, {
|
||||
displayName: recipientName,
|
||||
})}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:summaryReceivedForOrder`, { orderNr })}
|
||||
</Text>
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
|
||||
>
|
||||
<Button> {t(`${namespace}:linkText`, { orderNr })}</Button>
|
||||
</Link>
|
||||
<Text>
|
||||
{t(`${namespace}:ifButtonDisabled`)}{' '}
|
||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
|
||||
</Text>
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
to,
|
||||
};
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './emails/account-delete.email';
|
||||
export * from './emails/otp.email';
|
||||
export * from './emails/company-offer.email';
|
||||
export * from './emails/synlab.email';
|
||||
export * from './emails/doctor-summary-received.email';
|
||||
|
||||
@@ -0,0 +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:"
|
||||
}
|
||||
@@ -0,0 +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:"
|
||||
}
|
||||
8
packages/email-templates/src/locales/ru/common.json
Normal file
8
packages/email-templates/src/locales/ru/common.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"footer": {
|
||||
"lines1": "MedReport",
|
||||
"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>"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"subject": "Uus ettevõtte liitumispäring",
|
||||
"previewText": "Ettevõte {{companyName}} soovib pakkumist",
|
||||
"companyName": "Ettevõtte nimi:",
|
||||
"contactPerson": "Kontaktisik:",
|
||||
"email": "E-mail:",
|
||||
"phone": "Telefon:"
|
||||
}
|
||||
@@ -0,0 +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:"
|
||||
}
|
||||
12
packages/email-templates/src/locales/ru/synlab-email.json
Normal file
12
packages/email-templates/src/locales/ru/synlab-email.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"subject": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"hello": "Tere {{personName}},",
|
||||
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Synlab - {{partnerLocationName}}",
|
||||
"lines2": "<i>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>.</i>",
|
||||
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
||||
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
|
||||
"lines5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.",
|
||||
"lines6": "SYNLAB klienditoe telefon: <a href=\"tel:+37217123\">17123</a>"
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import { revalidatePath } from 'next/cache';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
import {
|
||||
NotificationAction,
|
||||
createNotificationLog,
|
||||
} from '../../../../../../../lib/services/audit/notificationEntries.service';
|
||||
import {
|
||||
DoctorAnalysisFeedbackTable,
|
||||
DoctorJobSelect,
|
||||
@@ -107,6 +111,7 @@ export const giveFeedbackAction = doctorAction(
|
||||
status: DoctorAnalysisFeedbackTable['status'];
|
||||
}) => {
|
||||
const logger = await getLogger();
|
||||
const isCompleted = status === 'COMPLETED';
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
@@ -118,8 +123,25 @@ export const giveFeedbackAction = doctorAction(
|
||||
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
|
||||
|
||||
revalidateDoctorAnalysis();
|
||||
|
||||
if (isCompleted) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: analysisOrderId,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
if (isCompleted) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED,
|
||||
status: 'FAIL',
|
||||
comment: e?.message,
|
||||
relatedRecordId: analysisOrderId,
|
||||
});
|
||||
}
|
||||
logger.error('Failed to give feedback', e);
|
||||
return { success: false, reason: ErrorReason.UNKNOWN };
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import 'server-only';
|
||||
|
||||
import { isBefore } from 'date-fns';
|
||||
|
||||
import { getFullName } from '@kit/shared/utils';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { sendDoctorSummaryCompletedEmail } from '../../../../../../../lib/services/mailer.service';
|
||||
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
|
||||
import {
|
||||
AnalysisResponseBase,
|
||||
@@ -635,5 +637,42 @@ export async function submitFeedback(
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
|
||||
if (status === 'COMPLETED') {
|
||||
const [{ data: recipient }, { data: medusaOrderIds }] = await Promise.all([
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('name, last_name, email, preferred_locale')
|
||||
.eq('is_personal_account', true)
|
||||
.eq('primary_owner_user_id', userId)
|
||||
.throwOnError(),
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.select('medusa_order_id, id')
|
||||
.eq('id', analysisOrderId)
|
||||
.limit(1)
|
||||
.throwOnError(),
|
||||
]);
|
||||
|
||||
if (!recipient?.[0]?.email) {
|
||||
throw new Error('Could not find user email.');
|
||||
}
|
||||
|
||||
if (!medusaOrderIds?.[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),
|
||||
email,
|
||||
medusaOrderIds?.[0]?.medusa_order_id ?? '',
|
||||
medusaOrderIds[0].id,
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function ConfirmationModal({
|
||||
<Trans i18nKey={descriptionKey} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogFooter className='gap-3'>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<Trans i18nKey={cancelKey} />
|
||||
</Button>
|
||||
|
||||
@@ -108,6 +108,90 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
medipost_dispatch: {
|
||||
Row: {
|
||||
changed_by: string | null
|
||||
created_at: string
|
||||
error_message: string | null
|
||||
id: number
|
||||
is_medipost_error: boolean
|
||||
is_success: boolean
|
||||
medusa_order_id: string
|
||||
}
|
||||
Insert: {
|
||||
changed_by?: string | null
|
||||
created_at?: string
|
||||
error_message?: string | null
|
||||
id?: number
|
||||
is_medipost_error: boolean
|
||||
is_success: boolean
|
||||
medusa_order_id: string
|
||||
}
|
||||
Update: {
|
||||
changed_by?: string | null
|
||||
created_at?: string
|
||||
error_message?: string | null
|
||||
id?: number
|
||||
is_medipost_error?: boolean
|
||||
is_success?: boolean
|
||||
medusa_order_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
medusa_action: {
|
||||
Row: {
|
||||
action: string
|
||||
created_at: string
|
||||
id: number
|
||||
medusa_user_id: string
|
||||
page: string | null
|
||||
user_email: string
|
||||
}
|
||||
Insert: {
|
||||
action: string
|
||||
created_at?: string
|
||||
id?: number
|
||||
medusa_user_id: string
|
||||
page?: string | null
|
||||
user_email: string
|
||||
}
|
||||
Update: {
|
||||
action?: string
|
||||
created_at?: string
|
||||
id?: number
|
||||
medusa_user_id?: string
|
||||
page?: string | null
|
||||
user_email?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
notification_entries: {
|
||||
Row: {
|
||||
action: string
|
||||
comment: string | null
|
||||
created_at: string
|
||||
id: number
|
||||
related_record_key: string | null
|
||||
status: Database["audit"]["Enums"]["action_status"]
|
||||
}
|
||||
Insert: {
|
||||
action: string
|
||||
comment?: string | null
|
||||
created_at?: string
|
||||
id?: number
|
||||
related_record_key?: string | null
|
||||
status: Database["audit"]["Enums"]["action_status"]
|
||||
}
|
||||
Update: {
|
||||
action?: string
|
||||
comment?: string | null
|
||||
created_at?: string
|
||||
id?: number
|
||||
related_record_key?: string | null
|
||||
status?: Database["audit"]["Enums"]["action_status"]
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
page_views: {
|
||||
Row: {
|
||||
account_id: string
|
||||
@@ -201,28 +285,6 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
medusa_action: {
|
||||
Row: {
|
||||
id: number
|
||||
medusa_user_id: string
|
||||
user_email: string
|
||||
action: string
|
||||
page: string
|
||||
created_at: string
|
||||
}
|
||||
Insert: {
|
||||
medusa_user_id: string
|
||||
user_email: string
|
||||
action: string
|
||||
page: string
|
||||
}
|
||||
Update: {
|
||||
medusa_user_id?: string
|
||||
user_email?: string
|
||||
action?: string
|
||||
page?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
@@ -231,6 +293,7 @@ export type Database = {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
action_status: "SUCCESS" | "FAIL"
|
||||
doctor_page_view_action:
|
||||
| "VIEW_ANALYSIS_RESULTS"
|
||||
| "VIEW_DASHBOARD"
|
||||
@@ -329,14 +392,14 @@ export type Database = {
|
||||
id: string
|
||||
is_personal_account: boolean
|
||||
last_name: string | null
|
||||
medusa_account_id: string | null
|
||||
name: string
|
||||
personal_code: string | null
|
||||
phone: string | null
|
||||
picture_url: string | null
|
||||
preferred_locale: Database["medreport"]["Enums"]["locale"] | null
|
||||
primary_owner_user_id: string
|
||||
public_data: Json
|
||||
slug: string | null
|
||||
medusa_account_id: string | null
|
||||
updated_at: string | null
|
||||
updated_by: string | null
|
||||
}
|
||||
@@ -351,14 +414,14 @@ export type Database = {
|
||||
id?: string
|
||||
is_personal_account?: boolean
|
||||
last_name?: string | null
|
||||
medusa_account_id?: string | null
|
||||
name: string
|
||||
personal_code?: string | null
|
||||
phone?: string | null
|
||||
picture_url?: string | null
|
||||
preferred_locale?: Database["medreport"]["Enums"]["locale"] | null
|
||||
primary_owner_user_id?: string
|
||||
public_data?: Json
|
||||
slug?: string | null
|
||||
medusa_account_id?: string | null
|
||||
updated_at?: string | null
|
||||
updated_by?: string | null
|
||||
}
|
||||
@@ -373,14 +436,14 @@ export type Database = {
|
||||
id?: string
|
||||
is_personal_account?: boolean
|
||||
last_name?: string | null
|
||||
medusa_account_id?: string | null
|
||||
name?: string
|
||||
personal_code?: string | null
|
||||
phone?: string | null
|
||||
picture_url?: string | null
|
||||
preferred_locale?: Database["medreport"]["Enums"]["locale"] | null
|
||||
primary_owner_user_id?: string
|
||||
public_data?: Json
|
||||
slug?: string | null
|
||||
medusa_account_id?: string | null
|
||||
updated_at?: string | null
|
||||
updated_by?: string | null
|
||||
}
|
||||
@@ -393,6 +456,7 @@ export type Database = {
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
has_seen_confirmation: boolean
|
||||
id: string
|
||||
updated_at: string
|
||||
updated_by: string | null
|
||||
user_id: string
|
||||
@@ -403,6 +467,7 @@ export type Database = {
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
has_seen_confirmation?: boolean
|
||||
id?: string
|
||||
updated_at?: string
|
||||
updated_by?: string | null
|
||||
user_id: string
|
||||
@@ -413,6 +478,7 @@ export type Database = {
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
has_seen_confirmation?: boolean
|
||||
id?: string
|
||||
updated_at?: string
|
||||
updated_by?: string | null
|
||||
user_id?: string
|
||||
@@ -1798,12 +1864,13 @@ export type Database = {
|
||||
id: string
|
||||
is_personal_account: boolean
|
||||
last_name: string | null
|
||||
medusa_account_id: string | null
|
||||
name: string
|
||||
personal_code: string | null
|
||||
phone: string | null
|
||||
picture_url: string | null
|
||||
preferred_locale: Database["medreport"]["Enums"]["locale"] | null
|
||||
primary_owner_user_id: string
|
||||
public_data: Json
|
||||
slug: string | null
|
||||
updated_at: string | null
|
||||
updated_by: string | null
|
||||
@@ -1836,6 +1903,7 @@ export type Database = {
|
||||
primary_owner_user_id: string
|
||||
name: string
|
||||
email: string
|
||||
personal_code: string
|
||||
picture_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -1853,10 +1921,18 @@ export type Database = {
|
||||
account_id: string
|
||||
}[]
|
||||
}
|
||||
get_medipost_dispatch_tries: {
|
||||
Args: { p_medusa_order_id: string }
|
||||
Returns: number
|
||||
}
|
||||
get_nonce_status: {
|
||||
Args: { p_id: string }
|
||||
Returns: Json
|
||||
}
|
||||
get_order_possible_actions: {
|
||||
Args: { p_medusa_order_id: string }
|
||||
Returns: Json
|
||||
}
|
||||
get_upper_system_role: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: string
|
||||
@@ -1937,6 +2013,10 @@ export type Database = {
|
||||
Args: { account_id: string; user_id: string }
|
||||
Returns: boolean
|
||||
}
|
||||
medipost_retry_dispatch: {
|
||||
Args: { order_id: string }
|
||||
Returns: Json
|
||||
}
|
||||
revoke_nonce: {
|
||||
Args: { p_id: string; p_reason?: string }
|
||||
Returns: boolean
|
||||
@@ -2057,21 +2137,6 @@ export type Database = {
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
medipost_retry_dispatch: {
|
||||
Args: {
|
||||
order_id: string
|
||||
}
|
||||
Returns: {
|
||||
success: boolean
|
||||
error: string | null
|
||||
}
|
||||
}
|
||||
get_medipost_dispatch_tries: {
|
||||
Args: {
|
||||
p_medusa_order_id: string
|
||||
}
|
||||
Returns: number
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"
|
||||
@@ -2093,6 +2158,7 @@ export type Database = {
|
||||
| "invites.manage"
|
||||
application_role: "user" | "doctor" | "super_admin"
|
||||
billing_provider: "stripe" | "lemon-squeezy" | "paddle" | "montonio"
|
||||
locale: "en" | "et" | "ru"
|
||||
notification_channel: "in_app" | "email"
|
||||
notification_type: "info" | "warning" | "error"
|
||||
payment_status: "pending" | "succeeded" | "failed"
|
||||
@@ -7959,6 +8025,7 @@ export type CompositeTypes<
|
||||
export const Constants = {
|
||||
audit: {
|
||||
Enums: {
|
||||
action_status: ["SUCCESS", "FAIL"],
|
||||
doctor_page_view_action: [
|
||||
"VIEW_ANALYSIS_RESULTS",
|
||||
"VIEW_DASHBOARD",
|
||||
@@ -7996,6 +8063,7 @@ export const Constants = {
|
||||
],
|
||||
application_role: ["user", "doctor", "super_admin"],
|
||||
billing_provider: ["stripe", "lemon-squeezy", "paddle", "montonio"],
|
||||
locale: ["en", "et", "ru"],
|
||||
notification_channel: ["in_app", "email"],
|
||||
notification_type: ["info", "warning", "error"],
|
||||
payment_status: ["pending", "succeeded", "failed"],
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { useUpdateAccountData } from '@kit/accounts/hooks/use-update-account';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../shadcn/select';
|
||||
} from '@kit/ui/select';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function LanguageSelector({
|
||||
onChange,
|
||||
@@ -19,6 +25,9 @@ export function LanguageSelector({
|
||||
}) {
|
||||
const { i18n } = useTranslation();
|
||||
const { language: currentLanguage, options } = i18n;
|
||||
const [value, setValue] = useState(i18n.language);
|
||||
|
||||
const { data: user } = useUser();
|
||||
|
||||
const locales = (options.supportedLngs as string[]).filter(
|
||||
(locale) => locale.toLowerCase() !== 'cimode',
|
||||
@@ -30,26 +39,37 @@ export function LanguageSelector({
|
||||
});
|
||||
}, [currentLanguage]);
|
||||
|
||||
const [value, setValue] = useState(i18n.language);
|
||||
const userId = user?.id;
|
||||
const updateAccountMutation = useUpdateAccountData(userId!);
|
||||
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
||||
|
||||
const languageChanged = useCallback(
|
||||
async (locale: string) => {
|
||||
const updateLanguagePreference = async (
|
||||
locale: Database['medreport']['Enums']['locale'],
|
||||
) => {
|
||||
setValue(locale);
|
||||
|
||||
if (onChange) {
|
||||
onChange(locale);
|
||||
}
|
||||
|
||||
const promise = updateAccountMutation
|
||||
.mutateAsync({
|
||||
preferred_locale: locale,
|
||||
})
|
||||
.then(() => {
|
||||
revalidateUserDataQuery(userId!);
|
||||
});
|
||||
await i18n.changeLanguage(locale);
|
||||
|
||||
// refresh cached translations
|
||||
window.location.reload();
|
||||
},
|
||||
[i18n, onChange],
|
||||
);
|
||||
return toast.promise(() => promise, {
|
||||
success: <Trans i18nKey={'account:updatePreferredLocaleSuccess'} />,
|
||||
error: <Trans i18nKey={'account:updatePreferredLocaleError'} />,
|
||||
loading: <Trans i18nKey={'account:updatePreferredLocaleLoading'} />,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Select value={value} onValueChange={languageChanged}>
|
||||
<Select value={value} onValueChange={updateLanguagePreference}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -125,5 +125,8 @@
|
||||
},
|
||||
"updateRoleSuccess": "Role updated",
|
||||
"updateRoleError": "Something went wrong, please try again",
|
||||
"updateRoleLoading": "Updating role..."
|
||||
"updateRoleLoading": "Updating role...",
|
||||
"updatePreferredLocaleSuccess": "Language preference updated",
|
||||
"updatePreferredLocaleError": "Language preference update failed",
|
||||
"updatePreferredLocaleLoading": "Updating language preference..."
|
||||
}
|
||||
@@ -148,5 +148,8 @@
|
||||
},
|
||||
"updateRoleSuccess": "Roll uuendatud",
|
||||
"updateRoleError": "Midagi läks valesti. Palun proovi uuesti",
|
||||
"updateRoleLoading": "Rolli uuendatakse..."
|
||||
"updateRoleLoading": "Rolli uuendatakse...",
|
||||
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
|
||||
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
|
||||
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse..."
|
||||
}
|
||||
@@ -125,5 +125,8 @@
|
||||
},
|
||||
"updateRoleSuccess": "Role updated",
|
||||
"updateRoleError": "Something went wrong, please try again",
|
||||
"updateRoleLoading": "Updating role..."
|
||||
"updateRoleLoading": "Updating role...",
|
||||
"updatePreferredLocaleSuccess": "Language preference updated",
|
||||
"updatePreferredLocaleError": "Language preference update failed",
|
||||
"updatePreferredLocaleLoading": "Updating language preference..."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE medreport.accounts
|
||||
DROP COLUMN IF EXISTS public_data;
|
||||
|
||||
create type medreport.locale as enum ('en', 'et', 'ru');
|
||||
|
||||
ALTER TABLE medreport.accounts
|
||||
ADD COLUMN preferred_locale medreport.locale
|
||||
@@ -0,0 +1,13 @@
|
||||
create type "audit"."action_status" as enum ('SUCCESS', 'FAIL');
|
||||
|
||||
create table audit.notification_entries (
|
||||
"id" bigint generated by default as identity not null,
|
||||
"status" audit.action_status not null,
|
||||
"action" text not null,
|
||||
"comment" text,
|
||||
"related_record_key" text,
|
||||
"created_at" timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
grant usage on schema audit to authenticated;
|
||||
grant select, insert on table audit.notification_entries to authenticated;
|
||||
Reference in New Issue
Block a user