diff --git a/app/doctor/_components/analysis-view.tsx b/app/doctor/_components/analysis-view.tsx index 9a369b1..47033a0 100644 --- a/app/doctor/_components/analysis-view.tsx +++ b/app/doctor/_components/analysis-view.tsx @@ -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({
-
{bmiFromMetric(patient?.weight ?? 0, patient?.height ?? 0)}
+
+ {patient?.weight && patient?.height + ? bmiFromMetric(patient.weight, patient.height) + : '-'} +
@@ -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" > @@ -253,7 +254,9 @@ export default function AnalysisView({ + + + {t(`${namespace}:ifButtonDisabled`)}{' '} + {`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`} + + + + + + + , + ); + + return { + html, + subject, + to, + }; +} diff --git a/packages/email-templates/src/index.ts b/packages/email-templates/src/index.ts index f9fe5c0..8407d1a 100644 --- a/packages/email-templates/src/index.ts +++ b/packages/email-templates/src/index.ts @@ -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'; diff --git a/packages/email-templates/src/locales/en/doctor-summary-received-email.json b/packages/email-templates/src/locales/en/doctor-summary-received-email.json new file mode 100644 index 0000000..ebefe9b --- /dev/null +++ b/packages/email-templates/src/locales/en/doctor-summary-received-email.json @@ -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:" +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/et/doctor-summary-received-email.json b/packages/email-templates/src/locales/et/doctor-summary-received-email.json new file mode 100644 index 0000000..e7efdc3 --- /dev/null +++ b/packages/email-templates/src/locales/et/doctor-summary-received-email.json @@ -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:" +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/ru/common.json b/packages/email-templates/src/locales/ru/common.json new file mode 100644 index 0000000..fc58e08 --- /dev/null +++ b/packages/email-templates/src/locales/ru/common.json @@ -0,0 +1,8 @@ +{ + "footer": { + "lines1": "MedReport", + "lines2": "E-mail: info@medreport.ee", + "lines3": "Klienditugi: +372 5887 1517", + "lines4": "www.medreport.ee" + } +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/ru/company-offer-email.json b/packages/email-templates/src/locales/ru/company-offer-email.json new file mode 100644 index 0000000..3a39792 --- /dev/null +++ b/packages/email-templates/src/locales/ru/company-offer-email.json @@ -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:" +} diff --git a/packages/email-templates/src/locales/ru/doctor-summary-received-email.json b/packages/email-templates/src/locales/ru/doctor-summary-received-email.json new file mode 100644 index 0000000..e7efdc3 --- /dev/null +++ b/packages/email-templates/src/locales/ru/doctor-summary-received-email.json @@ -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:" +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/ru/synlab-email.json b/packages/email-templates/src/locales/ru/synlab-email.json new file mode 100644 index 0000000..fa16c20 --- /dev/null +++ b/packages/email-templates/src/locales/ru/synlab-email.json @@ -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": "Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - vaata asukohti ja lahtiolekuaegasid.", + "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: saatekirjad alt eriarsti saatekiri.", + "lines5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.", + "lines6": "SYNLAB klienditoe telefon: 17123" +} \ No newline at end of file diff --git a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts index 47a4aac..4553578 100644 --- a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts +++ b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts @@ -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 }; } diff --git a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts index 771015b..9bc637a 100644 --- a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts +++ b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts @@ -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; } diff --git a/packages/shared/src/components/confirmation-modal.tsx b/packages/shared/src/components/confirmation-modal.tsx index aaa5421..4ba83b7 100644 --- a/packages/shared/src/components/confirmation-modal.tsx +++ b/packages/shared/src/components/confirmation-modal.tsx @@ -38,7 +38,7 @@ export default function ConfirmationModal({ - + diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 187a049..28d8341 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -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 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"], diff --git a/packages/ui/src/makerkit/language-selector.tsx b/packages/ui/src/makerkit/language-selector.tsx index b266858..59f07d0 100644 --- a/packages/ui/src/makerkit/language-selector.tsx +++ b/packages/ui/src/makerkit/language-selector.tsx @@ -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: , + error: , + loading: , + }); + }; return ( - diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 2939d6f..eab22ac 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -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..." +} \ No newline at end of file diff --git a/public/locales/et/account.json b/public/locales/et/account.json index 01dbddb..7c6e5cd 100644 --- a/public/locales/et/account.json +++ b/public/locales/et/account.json @@ -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..." +} \ No newline at end of file diff --git a/public/locales/ru/account.json b/public/locales/ru/account.json index cd0d74b..11125e1 100644 --- a/public/locales/ru/account.json +++ b/public/locales/ru/account.json @@ -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..." } \ No newline at end of file diff --git a/supabase/migrations/20250827044719_add_locale_to_account.sql b/supabase/migrations/20250827044719_add_locale_to_account.sql new file mode 100644 index 0000000..6ede041 --- /dev/null +++ b/supabase/migrations/20250827044719_add_locale_to_account.sql @@ -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 diff --git a/supabase/migrations/20250827080119_add_notification_audit_table.sql b/supabase/migrations/20250827080119_add_notification_audit_table.sql new file mode 100644 index 0000000..2563f6e --- /dev/null +++ b/supabase/migrations/20250827080119_add_notification_audit_table.sql @@ -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;