Merge branch 'main' into MED-85

This commit is contained in:
2025-08-28 14:57:09 +03:00
23 changed files with 561 additions and 179 deletions

View File

@@ -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,
};
}

View File

@@ -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';

View File

@@ -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:"
}

View File

@@ -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:"
}

View 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>"
}
}

View File

@@ -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:"
}

View File

@@ -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:"
}

View 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>"
}

View File

@@ -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 };
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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
@@ -204,28 +288,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
@@ -234,6 +296,7 @@ export type Database = {
[_ in never]: never
}
Enums: {
action_status: "SUCCESS" | "FAIL"
doctor_page_view_action:
| "VIEW_ANALYSIS_RESULTS"
| "VIEW_DASHBOARD"
@@ -332,14 +395,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
}
@@ -354,14 +417,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
}
@@ -376,14 +439,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
}
@@ -396,6 +459,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
@@ -406,6 +470,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
@@ -416,6 +481,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
@@ -1829,12 +1895,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
@@ -1867,6 +1934,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
@@ -1884,10 +1952,27 @@ export type Database = {
account_id: string
}[]
}
get_latest_medipost_dispatch_state_for_order: {
Args: {
medusa_order_id: string
}
Returns: {
has_success: boolean
action_date: 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
@@ -1968,6 +2053,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
@@ -2088,21 +2177,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
}
sync_analysis_results: {
}
send_medipost_test_response_for_order: {
@@ -2118,15 +2192,6 @@ export type Database = {
success: boolean
}
}
get_latest_medipost_dispatch_state_for_order: {
Args: {
medusa_order_id: string
}
Returns: {
has_success: boolean
action_date: string
}
}
}
Enums: {
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"
@@ -2148,6 +2213,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"
@@ -8014,6 +8080,7 @@ export type CompositeTypes<
export const Constants = {
audit: {
Enums: {
action_status: ["SUCCESS", "FAIL"],
doctor_page_view_action: [
"VIEW_ANALYSIS_RESULTS",
"VIEW_DASHBOARD",
@@ -8051,6 +8118,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"],

View File

@@ -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) => {
setValue(locale);
const updateLanguagePreference = async (
locale: Database['medreport']['Enums']['locale'],
) => {
setValue(locale);
if (onChange) {
onChange(locale);
}
if (onChange) {
onChange(locale);
}
await i18n.changeLanguage(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>