diff --git a/app/api/job/handler/sync-analysis-results.ts b/app/api/job/handler/sync-analysis-results.ts index c7fd529..c5a2323 100644 --- a/app/api/job/handler/sync-analysis-results.ts +++ b/app/api/job/handler/sync-analysis-results.ts @@ -58,5 +58,5 @@ export default async function syncAnalysisResults() { } return acc; }, {} as GroupedResults); - console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults, undefined, 2)}`); + console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`); } diff --git a/app/api/job/test-medipost-responses/route.ts b/app/api/job/test-medipost-responses/route.ts index 2cf8fa7..4ca8be0 100644 --- a/app/api/job/test-medipost-responses/route.ts +++ b/app/api/job/test-medipost-responses/route.ts @@ -16,7 +16,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } - const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'QUEUED' }); + const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'PROCESSING' }); console.error(`Sending test responses for ${analysisOrders.length} analysis orders`); for (const medreportOrder of analysisOrders) { 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 a070462..5040c87 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -165,6 +165,33 @@ export type Database = { } 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 @@ -266,6 +293,7 @@ export type Database = { [_ in never]: never } Enums: { + action_status: "SUCCESS" | "FAIL" doctor_page_view_action: | "VIEW_ANALYSIS_RESULTS" | "VIEW_DASHBOARD" @@ -369,8 +397,8 @@ export type Database = { 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 @@ -391,8 +419,8 @@ export type Database = { 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 @@ -413,8 +441,8 @@ export type Database = { 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 @@ -1839,8 +1867,8 @@ export type Database = { 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 @@ -2128,6 +2156,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" @@ -7994,6 +8023,7 @@ export type CompositeTypes< export const Constants = { audit: { Enums: { + action_status: ["SUCCESS", "FAIL"], doctor_page_view_action: [ "VIEW_ANALYSIS_RESULTS", "VIEW_DASHBOARD", @@ -8031,6 +8061,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/en/orders.json b/public/locales/en/orders.json index 7c16fc9..f846b0a 100644 --- a/public/locales/en/orders.json +++ b/public/locales/en/orders.json @@ -9,8 +9,7 @@ }, "status": { "QUEUED": "Waiting to send to lab", - "ON_HOLD": "Waiting for analysis results", - "PROCESSING": "In progress", + "PROCESSING": "Waiting for results", "PARTIAL_ANALYSIS_RESPONSE": "Partial analysis response", "FULL_ANALYSIS_RESPONSE": "All analysis responses received, waiting for doctor response", "COMPLETED": "Completed", 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/et/orders.json b/public/locales/et/orders.json index ef0a203..4811a2e 100644 --- a/public/locales/et/orders.json +++ b/public/locales/et/orders.json @@ -9,7 +9,6 @@ }, "status": { "QUEUED": "Esitatud", - "ON_HOLD": "Makstud", "PROCESSING": "Synlabile edastatud", "PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused", "FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes, ootab arsti kokkuvõtet", 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;