From 86dc221cc6ff9245f27bb7ea438d8fc883b5890b Mon Sep 17 00:00:00 2001 From: Helena <37183360+helenarebane@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:05:07 +0300 Subject: [PATCH 1/8] 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 --- app/doctor/_components/analysis-view.tsx | 37 ++-- .../audit/notificationEntries.service.ts | 35 ++++ lib/services/mailer.service.ts | 28 +++- lib/services/medusaCart.service.ts | 158 +++++++++--------- .../emails/doctor-summary-received.email.tsx | 97 +++++++++++ packages/email-templates/src/index.ts | 1 + .../en/doctor-summary-received-email.json | 8 + .../et/doctor-summary-received-email.json | 8 + .../src/locales/ru/common.json | 8 + .../src/locales/ru/company-offer-email.json | 8 + .../ru/doctor-summary-received-email.json | 8 + .../src/locales/ru/synlab-email.json | 12 ++ .../server/actions/doctor-server-actions.ts | 24 ++- .../services/doctor-analysis.service.ts | 39 +++++ .../src/components/confirmation-modal.tsx | 2 +- packages/supabase/src/database.types.ts | 156 ++++++++++++----- .../ui/src/makerkit/language-selector.tsx | 52 ++++-- public/locales/en/account.json | 7 +- public/locales/et/account.json | 7 +- public/locales/ru/account.json | 5 +- .../20250827044719_add_locale_to_account.sql | 7 + ...827080119_add_notification_audit_table.sql | 13 ++ 22 files changed, 551 insertions(+), 169 deletions(-) create mode 100644 lib/services/audit/notificationEntries.service.ts create mode 100644 packages/email-templates/src/emails/doctor-summary-received.email.tsx create mode 100644 packages/email-templates/src/locales/en/doctor-summary-received-email.json create mode 100644 packages/email-templates/src/locales/et/doctor-summary-received-email.json create mode 100644 packages/email-templates/src/locales/ru/common.json create mode 100644 packages/email-templates/src/locales/ru/company-offer-email.json create mode 100644 packages/email-templates/src/locales/ru/doctor-summary-received-email.json create mode 100644 packages/email-templates/src/locales/ru/synlab-email.json create mode 100644 supabase/migrations/20250827044719_add_locale_to_account.sql create mode 100644 supabase/migrations/20250827080119_add_notification_audit_table.sql 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 4137545..6bf2bb8 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 @@ -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 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"], 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; From 31bc4b6cffd7cff1d90855bc8cf00b513b2d4d0a Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Thu, 28 Aug 2025 13:15:39 +0300 Subject: [PATCH 2/8] initial commit --- app/api/job/handler/sync-connected-online.ts | 18 +-- .../(dashboard)/booking/[handle]/page.tsx | 33 +++++ app/home/(user)/(dashboard)/booking/page.tsx | 10 +- app/home/(user)/_components/order-cards.tsx | 94 ++++++------- .../(user)/_components/service-categories.tsx | 61 ++++++++ app/home/(user)/_lib/server/load-analyses.ts | 49 ++++--- .../(user)/_lib/server/load-tto-services.ts | 49 +++++++ .../src/lib/data/categories.ts | 45 +++--- packages/shared/src/config/paths.config.ts | 2 + packages/supabase/src/database.types.ts | 133 +++++++++++------- .../migrations/20250827134000_bookings.sql | 2 + 11 files changed, 345 insertions(+), 151 deletions(-) create mode 100644 app/home/(user)/(dashboard)/booking/[handle]/page.tsx create mode 100644 app/home/(user)/_components/service-categories.tsx create mode 100644 app/home/(user)/_lib/server/load-tto-services.ts create mode 100644 supabase/migrations/20250827134000_bookings.sql diff --git a/app/api/job/handler/sync-connected-online.ts b/app/api/job/handler/sync-connected-online.ts index 829ba54..39a5fb3 100644 --- a/app/api/job/handler/sync-connected-online.ts +++ b/app/api/job/handler/sync-connected-online.ts @@ -71,19 +71,19 @@ export default async function syncConnectedOnline() { return { id: service.ID, clinic_id: service.ClinicID, - code: service.Code, - description: service.Description || null, - display: service.Display, - duration: service.Duration, - has_free_codes: !!service.HasFreeCodes, + sync_id: service.SyncID, name: service.Name, + description: service.Description || null, + price: service.Price, + requires_payment: !!service.RequiresPayment, + duration: service.Duration, neto_duration: service.NetoDuration, + display: service.Display, + price_periods: service.PricePeriods || null, online_hide_duration: service.OnlineHideDuration, online_hide_price: service.OnlineHidePrice, - price: service.Price, - price_periods: service.PricePeriods || null, - requires_payment: !!service.RequiresPayment, - sync_id: service.SyncID, + code: service.Code, + has_free_codes: !!service.HasFreeCodes, }; }); diff --git a/app/home/(user)/(dashboard)/booking/[handle]/page.tsx b/app/home/(user)/(dashboard)/booking/[handle]/page.tsx new file mode 100644 index 0000000..9036447 --- /dev/null +++ b/app/home/(user)/(dashboard)/booking/[handle]/page.tsx @@ -0,0 +1,33 @@ +import { use } from 'react'; + +import { PageBody } from '@kit/ui/page'; +import { Trans } from '@kit/ui/trans'; + +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +import { HomeLayoutPageHeader } from '../../../_components/home-page-header'; + +export const generateMetadata = async () => { + const i18n = await createI18nServerInstance(); + const title = i18n.t('booking:title'); + + return { + title, + }; +}; + +function BookingHandlePage() { + return ( + <> + } + description={} + /> + + + + ); +} + +export default withI18n(BookingHandlePage); diff --git a/app/home/(user)/(dashboard)/booking/page.tsx b/app/home/(user)/(dashboard)/booking/page.tsx index def99c9..14b1c1a 100644 --- a/app/home/(user)/(dashboard)/booking/page.tsx +++ b/app/home/(user)/(dashboard)/booking/page.tsx @@ -1,12 +1,15 @@ +import { use } from 'react'; + import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; - import { HomeLayoutPageHeader } from '../../_components/home-page-header'; import OrderCards from '../../_components/order-cards'; +import ServiceCategories from '../../_components/service-categories'; +import { loadTtoServices } from '../../_lib/server/load-tto-services'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -18,6 +21,8 @@ export const generateMetadata = async () => { }; function BookingPage() { + const { heroCategories, ttoCategories } = use(loadTtoServices()); + console.log('ttoCategories', heroCategories, ttoCategories); return ( <> - + + ); diff --git a/app/home/(user)/_components/order-cards.tsx b/app/home/(user)/_components/order-cards.tsx index bf95ace..b179d18 100644 --- a/app/home/(user)/_components/order-cards.tsx +++ b/app/home/(user)/_components/order-cards.tsx @@ -1,74 +1,68 @@ -"use client"; +'use client'; -import { ChevronRight, HeartPulse } from 'lucide-react'; import Link from 'next/link'; +import { cn } from '@/lib/utils'; +import { pathsConfig } from '@/packages/shared/src/config'; +import { ComponentInstanceIcon } from '@radix-ui/react-icons'; +import { ChevronRight, HeartPulse } from 'lucide-react'; + import { Button } from '@kit/ui/button'; import { Card, - CardHeader, CardDescription, - CardProps, CardFooter, + CardHeader, + CardProps, } from '@kit/ui/card'; -import { Trans } from '@kit/ui/trans'; -import { cn } from '@/lib/utils'; -const dummyCards = [ - { - title: 'booking:analysisPackages.title', - description: 'booking:analysisPackages.description', - descriptionColor: 'text-primary', - icon: ( - - - - ), - cardVariant: 'gradient-success' as CardProps['variant'], - iconBg: 'bg-warning', - }, -]; +import { ServiceCategory } from './service-categories'; -export default function OrderCards() { +export default function OrderCards({ + heroCategories, +}: { + heroCategories: ServiceCategory[]; +}) { return ( -
- {dummyCards.map(({ - title, - description, - icon, - cardVariant, - descriptionColor, - iconBg, - }) => ( +
+ {heroCategories.map(({ name, description, color, handle }) => ( - +
- {icon} + +
+
+ + +
- -
- -
-
- -
- - - + +
{name}
+ {description}
))} diff --git a/app/home/(user)/_components/service-categories.tsx b/app/home/(user)/_components/service-categories.tsx new file mode 100644 index 0000000..148f109 --- /dev/null +++ b/app/home/(user)/_components/service-categories.tsx @@ -0,0 +1,61 @@ +'use client'; + +import React from 'react'; + +import { redirect } from 'next/navigation'; + +import { createPath, pathsConfig } from '@/packages/shared/src/config'; +import { ComponentInstanceIcon } from '@radix-ui/react-icons'; + +import { cn } from '@kit/ui/shadcn'; +import { Card, CardDescription, CardTitle } from '@kit/ui/shadcn/card'; + +export interface ServiceCategory { + name: string; + handle: string; + color: string; + description: string; +} + +const ServiceCategories = ({ + categories, +}: { + categories: ServiceCategory[]; +}) => { + return ( +
+ {categories.map((category, index) => ( + { + redirect( + pathsConfig.app.bookingHandle.replace( + '[handle]', + category.handle, + ), + ); + }} + > +
+ +
+
+
{category.name}
+ + {category.description} + +
+
+ ))} +
+ ); +}; + +export default ServiceCategories; diff --git a/app/home/(user)/_lib/server/load-analyses.ts b/app/home/(user)/_lib/server/load-analyses.ts index 424ff25..db22011 100644 --- a/app/home/(user)/_lib/server/load-analyses.ts +++ b/app/home/(user)/_lib/server/load-analyses.ts @@ -1,9 +1,11 @@ import { cache } from 'react'; -import { listProductTypes } from "@lib/data/products"; -import { listRegions } from '@lib/data/regions'; import { getProductCategories } from '@lib/data/categories'; +import { listProductTypes } from '@lib/data/products'; +import { listRegions } from '@lib/data/regions'; + import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; +import { ServiceCategory } from '../../_components/service-categories'; async function countryCodesLoader() { const countryCodes = await listRegions().then((regions) => @@ -14,7 +16,9 @@ async function countryCodesLoader() { export const loadCountryCodes = cache(countryCodesLoader); async function productCategoriesLoader() { - const productCategories = await getProductCategories({ fields: "*products, *products.variants" }); + const productCategories = await getProductCategories({ + fields: '*products, *products.variants, is_active', + }); return productCategories.product_categories ?? []; } export const loadProductCategories = cache(productCategoriesLoader); @@ -29,25 +33,34 @@ async function analysesLoader() { const [countryCodes, productCategories] = await Promise.all([ loadCountryCodes(), loadProductCategories(), - ]); + ]); const countryCode = countryCodes[0]!; - const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis'); - + const category = productCategories.find( + ({ metadata }) => metadata?.page === 'order-analysis', + ); + const serviceCategories = productCategories.filter( + ({ parent_category }) => parent_category?.handle === 'tto-categories', + ); + console.log('serviceCategories', serviceCategories); return { - analyses: category?.products?.map(({ title, description, subtitle, variants, status, metadata }) => { - const variant = variants![0]!; - return { - title, - description, - subtitle, - variant: { - id: variant.id, + analyses: + category?.products?.map( + ({ title, description, subtitle, variants, status, metadata }) => { + const variant = variants![0]!; + return { + title, + description, + subtitle, + variant: { + id: variant.id, + }, + isAvailable: + status === 'published' && !!metadata?.analysisIdOriginal, + }; }, - isAvailable: status === 'published' && !!metadata?.analysisIdOriginal, - }; - }) ?? [], + ) ?? [], countryCode, - } + }; } export const loadAnalyses = cache(analysesLoader); diff --git a/app/home/(user)/_lib/server/load-tto-services.ts b/app/home/(user)/_lib/server/load-tto-services.ts new file mode 100644 index 0000000..a8b4e1b --- /dev/null +++ b/app/home/(user)/_lib/server/load-tto-services.ts @@ -0,0 +1,49 @@ +import { cache } from 'react'; + +import { getProductCategories } from '@lib/data'; + +import { ServiceCategory } from '../../_components/service-categories'; + +async function ttoServicesLoader() { + const response = await getProductCategories({ + fields: '*products, is_active, metadata', + }); + console.log('response.product_categories', response.product_categories); + const heroCategories = response.product_categories?.filter( + ({ parent_category, is_active, metadata }) => + parent_category?.handle === 'tto-categories' && + is_active && + metadata?.isHero, + ); + + const ttoCategories = response.product_categories?.filter( + ({ parent_category, is_active, metadata }) => + parent_category?.handle === 'tto-categories' && + is_active && + !metadata?.isHero, + ); + + return { + heroCategories: + heroCategories.map( + ({ name, handle, metadata, description }) => ({ + name, + handle, + color: + typeof metadata?.color === 'string' ? metadata.color : 'primary', + description, + }), + ) ?? [], + ttoCategories: + ttoCategories.map( + ({ name, handle, metadata, description }) => ({ + name, + handle, + color: + typeof metadata?.color === 'string' ? metadata.color : 'primary', + description, + }), + ) ?? [], + }; +} +export const loadTtoServices = cache(ttoServicesLoader); diff --git a/packages/features/medusa-storefront/src/lib/data/categories.ts b/packages/features/medusa-storefront/src/lib/data/categories.ts index 7b3987d..b4db69d 100644 --- a/packages/features/medusa-storefront/src/lib/data/categories.ts +++ b/packages/features/medusa-storefront/src/lib/data/categories.ts @@ -1,13 +1,13 @@ -import { sdk } from "@lib/config" -import { HttpTypes } from "@medusajs/types" -import { getCacheOptions } from "./cookies" +import { sdk } from "@lib/config"; +import { HttpTypes } from "@medusajs/types"; +import { getCacheOptions } from "./cookies"; export const listCategories = async (query?: Record) => { const next = { ...(await getCacheOptions("categories")), - } + }; - const limit = query?.limit || 100 + const limit = query?.limit || 100; return sdk.client .fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>( @@ -23,8 +23,8 @@ export const listCategories = async (query?: Record) => { cache: "force-cache", } ) - .then(({ product_categories }) => product_categories) -} + .then(({ product_categories }) => product_categories); +}; export const getCategoryByHandle = async (categoryHandle: string[]) => { const { product_categories } = await getProductCategories({ @@ -32,7 +32,7 @@ export const getCategoryByHandle = async (categoryHandle: string[]) => { limit: 1, }); return product_categories[0]; -} +}; export const getProductCategories = async ({ handle, @@ -45,19 +45,18 @@ export const getProductCategories = async ({ } = {}) => { const next = { ...(await getCacheOptions("categories")), - } + }; - return sdk.client - .fetch( - `/store/product-categories`, - { - query: { - fields, - handle, - limit, - }, - next, - //cache: "force-cache", - } - ); -} + return sdk.client.fetch( + `/store/product-categories`, + { + query: { + fields, + handle, + limit, + }, + next, + //cache: "force-cache", + } + ); +}; diff --git a/packages/shared/src/config/paths.config.ts b/packages/shared/src/config/paths.config.ts index d21c445..4e400e4 100644 --- a/packages/shared/src/config/paths.config.ts +++ b/packages/shared/src/config/paths.config.ts @@ -16,6 +16,7 @@ const PathsSchema = z.object({ home: z.string().min(1), selectPackage: z.string().min(1), booking: z.string().min(1), + bookingHandle: z.string().min(1), myOrders: z.string().min(1), analysisResults: z.string().min(1), orderAnalysisPackage: z.string().min(1), @@ -64,6 +65,7 @@ const pathsConfig = PathsSchema.parse({ joinTeam: '/join', selectPackage: '/select-package', booking: '/home/booking', + bookingHandle: '/home/booking/[handle]', orderAnalysisPackage: '/home/order-analysis-package', myOrders: '/home/order', analysisResults: '/home/analysis-results', diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 4137545..a070462 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -108,6 +108,63 @@ 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: [] + } page_views: { Row: { account_id: string @@ -201,28 +258,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 @@ -329,6 +364,7 @@ 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 @@ -336,7 +372,6 @@ export type Database = { 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,6 +386,7 @@ 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 @@ -358,7 +394,6 @@ export type Database = { 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,6 +408,7 @@ 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 @@ -380,7 +416,6 @@ export type Database = { 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 +428,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 +439,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 +450,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 @@ -1022,7 +1060,7 @@ export type Database = { price: number price_periods: string | null requires_payment: boolean - sync_id: number + sync_id: string | null updated_at: string | null } Insert: { @@ -1041,7 +1079,7 @@ export type Database = { price: number price_periods?: string | null requires_payment: boolean - sync_id: number + sync_id?: string | null updated_at?: string | null } Update: { @@ -1060,7 +1098,7 @@ export type Database = { price?: number price_periods?: string | null requires_payment?: boolean - sync_id?: number + sync_id?: string | null updated_at?: string | null } Relationships: [ @@ -1081,7 +1119,7 @@ export type Database = { doctor_user_id: string | null id: number status: Database["medreport"]["Enums"]["analysis_feedback_status"] - updated_at: string + updated_at: string | null updated_by: string | null user_id: string value: string | null @@ -1093,7 +1131,7 @@ export type Database = { doctor_user_id?: string | null id?: number status?: Database["medreport"]["Enums"]["analysis_feedback_status"] - updated_at?: string + updated_at?: string | null updated_by?: string | null user_id: string value?: string | null @@ -1105,7 +1143,7 @@ export type Database = { doctor_user_id?: string | null id?: number status?: Database["medreport"]["Enums"]["analysis_feedback_status"] - updated_at?: string + updated_at?: string | null updated_by?: string | null user_id?: string value?: string | null @@ -1784,9 +1822,7 @@ export type Database = { Returns: Json } create_team_account: { - Args: - | { account_name: string } - | { account_name: string; new_personal_code: string } + Args: { account_name: string; new_personal_code: string } Returns: { application_role: Database["medreport"]["Enums"]["application_role"] city: string | null @@ -1798,6 +1834,7 @@ 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 @@ -1836,6 +1873,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 +1891,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 Returns: string @@ -1937,6 +1983,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 +2107,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" diff --git a/supabase/migrations/20250827134000_bookings.sql b/supabase/migrations/20250827134000_bookings.sql new file mode 100644 index 0000000..9d77627 --- /dev/null +++ b/supabase/migrations/20250827134000_bookings.sql @@ -0,0 +1,2 @@ +ALTER TABLE medreport.connected_online_services +ALTER COLUMN sync_id TYPE text USING sync_id::text; \ No newline at end of file From ad28352fc8780048617c1aa94b92d3544b8f2dd9 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Thu, 28 Aug 2025 14:11:54 +0300 Subject: [PATCH 3/8] MED-104: create booking view with categories --- .../(dashboard)/booking/[handle]/page.tsx | 16 +++++++--- app/home/(user)/(dashboard)/booking/page.tsx | 17 ++++++++-- app/home/(user)/_components/order-cards.tsx | 2 +- .../(user)/_components/service-categories.tsx | 2 +- app/home/(user)/_lib/server/load-analyses.ts | 2 +- app/home/(user)/_lib/server/load-category.ts | 31 +++++++++++++++++++ .../(user)/_lib/server/load-tto-services.ts | 2 +- public/locales/et/booking.json | 15 ++++----- public/locales/et/common.json | 3 +- 9 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 app/home/(user)/_lib/server/load-category.ts diff --git a/app/home/(user)/(dashboard)/booking/[handle]/page.tsx b/app/home/(user)/(dashboard)/booking/[handle]/page.tsx index 9036447..ebce187 100644 --- a/app/home/(user)/(dashboard)/booking/[handle]/page.tsx +++ b/app/home/(user)/(dashboard)/booking/[handle]/page.tsx @@ -1,13 +1,13 @@ -import { use } from 'react'; +import { HomeLayoutPageHeader } from '@/app/home/(user)/_components/home-page-header'; +import { loadCategory } from '@/app/home/(user)/_lib/server/load-category'; +import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { HomeLayoutPageHeader } from '../../../_components/home-page-header'; - export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); const title = i18n.t('booking:title'); @@ -17,9 +17,17 @@ export const generateMetadata = async () => { }; }; -function BookingHandlePage() { +async function BookingHandlePage({ params }: { params: { handle: string } }) { + const handle = await params.handle; + const { category } = await loadCategory({ handle }); + return ( <> + } description={} diff --git a/app/home/(user)/(dashboard)/booking/page.tsx b/app/home/(user)/(dashboard)/booking/page.tsx index 14b1c1a..680edfb 100644 --- a/app/home/(user)/(dashboard)/booking/page.tsx +++ b/app/home/(user)/(dashboard)/booking/page.tsx @@ -1,5 +1,6 @@ import { use } from 'react'; +import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; @@ -22,15 +23,27 @@ export const generateMetadata = async () => { function BookingPage() { const { heroCategories, ttoCategories } = use(loadTtoServices()); - console.log('ttoCategories', heroCategories, ttoCategories); + + if (!heroCategories.length && !ttoCategories.length) { + return ( + <> + +

+ +

+ + ); + } + return ( <> + } description={} /> - + diff --git a/app/home/(user)/_components/order-cards.tsx b/app/home/(user)/_components/order-cards.tsx index b179d18..0c936ce 100644 --- a/app/home/(user)/_components/order-cards.tsx +++ b/app/home/(user)/_components/order-cards.tsx @@ -24,7 +24,7 @@ export default function OrderCards({ heroCategories: ServiceCategory[]; }) { return ( -
+
{heroCategories.map(({ name, description, color, handle }) => ( { return ( -
+
{categories.map((category, index) => ( parent_category?.handle === 'tto-categories', ); - console.log('serviceCategories', serviceCategories); + return { analyses: category?.products?.map( diff --git a/app/home/(user)/_lib/server/load-category.ts b/app/home/(user)/_lib/server/load-category.ts new file mode 100644 index 0000000..2c0479c --- /dev/null +++ b/app/home/(user)/_lib/server/load-category.ts @@ -0,0 +1,31 @@ +import { cache } from 'react'; + +import { getProductCategories } from '@lib/data'; + +import { ServiceCategory } from '../../_components/service-categories'; + +async function categoryLoader({ + handle, +}: { + handle: string; +}): Promise<{ category: ServiceCategory | null }> { + const response = await getProductCategories({ + handle, + fields: '*products, is_active, metadata', + }); + + const category = response.product_categories[0]; + + return { + category: { + color: + typeof category?.metadata?.color === 'string' + ? category?.metadata?.color + : 'primary', + description: category?.description || '', + handle: category?.handle || '', + name: category?.name || '', + }, + }; +} +export const loadCategory = cache(categoryLoader); diff --git a/app/home/(user)/_lib/server/load-tto-services.ts b/app/home/(user)/_lib/server/load-tto-services.ts index a8b4e1b..3bbc4e5 100644 --- a/app/home/(user)/_lib/server/load-tto-services.ts +++ b/app/home/(user)/_lib/server/load-tto-services.ts @@ -8,7 +8,7 @@ async function ttoServicesLoader() { const response = await getProductCategories({ fields: '*products, is_active, metadata', }); - console.log('response.product_categories', response.product_categories); + const heroCategories = response.product_categories?.filter( ({ parent_category, is_active, metadata }) => parent_category?.handle === 'tto-categories' && diff --git a/public/locales/et/booking.json b/public/locales/et/booking.json index 17554de..3410d59 100644 --- a/public/locales/et/booking.json +++ b/public/locales/et/booking.json @@ -1,8 +1,9 @@ { - "title": "Vali teenus", - "description": "Vali sobiv teenus või pakett vastavalt oma tervisemurele või -eesmärgile.", - "analysisPackages": { - "title": "Analüüside paketid", - "description": "Tutvu personaalsete analüüsi pakettidega ja telli" - } -} \ No newline at end of file + "title": "Vali teenus", + "description": "Vali sobiv teenus või pakett vastavalt oma tervisemurele või -eesmärgile.", + "analysisPackages": { + "title": "Analüüside paketid", + "description": "Tutvu personaalsete analüüsi pakettidega ja telli" + }, + "noCategories": "Teenuste loetelu ei leitud, proovi hiljem uuesti" +} diff --git a/public/locales/et/common.json b/public/locales/et/common.json index 8239aaf..70e6ec6 100644 --- a/public/locales/et/common.json +++ b/public/locales/et/common.json @@ -80,7 +80,8 @@ "dashboard": "Ülevaade", "settings": "Settings", "profile": "Profile", - "application": "Application" + "application": "Application", + "pickTime": "Vali aeg" }, "roles": { "owner": { From e0688eb539d4bf5f19daf86b06adae4e83d25dce Mon Sep 17 00:00:00 2001 From: Karli Date: Thu, 28 Aug 2025 15:49:03 +0300 Subject: [PATCH 4/8] feat(MED-86): fix status check for fake responses --- app/api/job/test-medipost-responses/route.ts | 2 +- public/locales/en/orders.json | 3 +-- public/locales/et/orders.json | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) 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/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/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", From 4588d11d5a554e9cb28a4a1e8028166b54b67d2f Mon Sep 17 00:00:00 2001 From: Karli Date: Thu, 28 Aug 2025 15:37:29 +0300 Subject: [PATCH 5/8] feat(MED-86): don't prettify results sync log since aws will split it up --- app/api/job/handler/sync-analysis-results.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)}`); } From 5ee161f482c7d5a039eadd09a0f15dc5fba156c1 Mon Sep 17 00:00:00 2001 From: Helena Date: Thu, 28 Aug 2025 16:47:27 +0300 Subject: [PATCH 6/8] remove html length limit from emails --- lib/validations/email.schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validations/email.schema.ts b/lib/validations/email.schema.ts index 58cc00d..b06524d 100644 --- a/lib/validations/email.schema.ts +++ b/lib/validations/email.schema.ts @@ -3,5 +3,5 @@ import { z } from 'zod'; export const emailSchema = z.object({ to: z.string().email(), subject: z.string().min(1).max(200), - html: z.string().min(1).max(5000), + html: z.string().min(1), }); From 7d1400fba6ffd40342d757029c9f2ca5978088da Mon Sep 17 00:00:00 2001 From: Helena Date: Thu, 28 Aug 2025 17:30:32 +0300 Subject: [PATCH 7/8] log email result and subject --- lib/services/mailer.service.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/services/mailer.service.ts b/lib/services/mailer.service.ts index 07ccc37..74bc165 100644 --- a/lib/services/mailer.service.ts +++ b/lib/services/mailer.service.ts @@ -6,6 +6,7 @@ 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 { getLogger } from '@kit/shared/logger'; export const sendDoctorSummaryCompletedEmail = async ( language: string, @@ -49,12 +50,18 @@ export const sendCompanyOfferEmail = async ( export const sendEmail = enhanceAction( async ({ subject, html, to }) => { const mailer = await getMailer(); - await mailer.sendEmail({ + const log = await getLogger(); + + const result = await mailer.sendEmail({ to, subject, html, }); + log.info( + `Sent email with subject "${subject}", result: ${JSON.stringify(result)}`, + ); + return {}; }, { From 5cf29447b302900fe82cfb2da7ef4e672f18d479 Mon Sep 17 00:00:00 2001 From: Helena Date: Thu, 28 Aug 2025 18:03:28 +0300 Subject: [PATCH 8/8] add email sender --- lib/services/mailer.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/services/mailer.service.ts b/lib/services/mailer.service.ts index 74bc165..c902bad 100644 --- a/lib/services/mailer.service.ts +++ b/lib/services/mailer.service.ts @@ -52,7 +52,13 @@ export const sendEmail = enhanceAction( const mailer = await getMailer(); const log = await getLogger(); + if (!process.env.EMAIL_USER) { + log.error('Sending email failed, as no sender found in env.') + throw new Error('No email user configured'); + } + const result = await mailer.sendEmail({ + from: process.env.EMAIL_USER, to, subject, html,