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/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/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({ - - ), - 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..9ef3e25 --- /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..12eebbf 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', + ); 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-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 new file mode 100644 index 0000000..3bbc4e5 --- /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', + }); + + 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/lib/services/audit/notificationEntries.service.ts b/lib/services/audit/notificationEntries.service.ts new file mode 100644 index 0000000..fee0b75 --- /dev/null +++ b/lib/services/audit/notificationEntries.service.ts @@ -0,0 +1,35 @@ +import { Database } from '@kit/supabase/database'; +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + +export enum NotificationAction { + DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED', +} + +export const createNotificationLog = async ({ + action, + status, + comment, + relatedRecordId, +}: { + action: NotificationAction; + status: Database['audit']['Enums']['action_status']; + comment?: string; + relatedRecordId?: string | number; +}) => { + try { + const supabase = getSupabaseServerClient(); + + await supabase + .schema('audit') + .from('notification_entries') + .insert({ + action, + status, + comment, + related_record_key: relatedRecordId?.toString(), + }) + .throwOnError(); + } catch (error) { + console.error('Failed to insert doctor page view log', error); + } +}; diff --git a/lib/services/mailer.service.ts b/lib/services/mailer.service.ts index 52a8749..c902bad 100644 --- a/lib/services/mailer.service.ts +++ b/lib/services/mailer.service.ts @@ -1,10 +1,34 @@ 'use server'; +import { CompanySubmitData } from '@/lib/types/company'; +import { emailSchema } from '@/lib/validations/email.schema'; + +import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates'; import { getMailer } from '@kit/mailers'; import { enhanceAction } from '@kit/next/actions'; +import { getLogger } from '@kit/shared/logger'; -import { CompanySubmitData } from '../types/company'; -import { emailSchema } from '../validations/email.schema'; +export const sendDoctorSummaryCompletedEmail = async ( + language: string, + recipientName: string, + recipientEmail: string, + orderNr: string, + orderId: number, +) => { + const { html, subject } = await renderDoctorSummaryReceivedEmail({ + language, + recipientName, + recipientEmail, + orderNr, + orderId, + }); + + await sendEmail({ + subject, + html, + to: recipientEmail, + }); +}; export const sendCompanyOfferEmail = async ( data: CompanySubmitData, @@ -26,13 +50,24 @@ export const sendCompanyOfferEmail = async ( export const sendEmail = enhanceAction( async ({ subject, html, to }) => { const mailer = await getMailer(); + const log = await getLogger(); - await mailer.sendEmail({ + 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, }); + log.info( + `Sent email with subject "${subject}", result: ${JSON.stringify(result)}`, + ); + return {}; }, { diff --git a/lib/services/medusaCart.service.ts b/lib/services/medusaCart.service.ts index e838c79..a2eef85 100644 --- a/lib/services/medusaCart.service.ts +++ b/lib/services/medusaCart.service.ts @@ -1,42 +1,45 @@ 'use server'; -import { z } from 'zod'; import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account'; -import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src'; import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart'; import { getCartId } from '@lib/data/cookies'; import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types'; -import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src'; +import { z } from 'zod'; + +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + import { requireUserInServerComponent } from '../server/require-user-in-server-component'; -const env = () => z - .object({ - medusaBackendPublicUrl: z - .string({ - required_error: 'MEDUSA_BACKEND_PUBLIC_URL is required', - }) - .min(1), - siteUrl: z - .string({ - required_error: 'NEXT_PUBLIC_SITE_URL is required', - }) - .min(1), - }) - .parse({ - medusaBackendPublicUrl: process.env.MEDUSA_BACKEND_PUBLIC_URL!, - siteUrl: process.env.NEXT_PUBLIC_SITE_URL!, - }); +const env = () => + z + .object({ + medusaBackendPublicUrl: z + .string({ + required_error: 'MEDUSA_BACKEND_PUBLIC_URL is required', + }) + .min(1), + siteUrl: z + .string({ + required_error: 'NEXT_PUBLIC_SITE_URL is required', + }) + .min(1), + }) + .parse({ + medusaBackendPublicUrl: process.env.MEDUSA_BACKEND_PUBLIC_URL!, + siteUrl: process.env.NEXT_PUBLIC_SITE_URL!, + }); export async function handleAddToCart({ selectedVariant, countryCode, }: { - selectedVariant: Pick - countryCode: string + selectedVariant: Pick; + countryCode: string; }) { const supabase = getSupabaseServerClient(); const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount() + const account = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } @@ -48,16 +51,13 @@ export async function handleAddToCart({ countryCode, }); - const { error } = await supabase - .schema('audit') - .from('cart_entries') - .insert({ - variant_id: selectedVariant.id, - operation: 'ADD_TO_CART', - account_id: account.id, - cart_id: cart.id, - changed_by: user.id, - }); + const { error } = await supabase.schema('audit').from('cart_entries').insert({ + variant_id: selectedVariant.id, + operation: 'ADD_TO_CART', + account_id: account.id, + cart_id: cart.id, + changed_by: user.id, + }); if (error) { throw new Error('Error logging cart entry: ' + error.message); } @@ -65,68 +65,65 @@ export async function handleAddToCart({ return cart; } -export async function handleDeleteCartItem({ - lineId, -}: { - lineId: string; -}) { +export async function handleDeleteCartItem({ lineId }: { lineId: string }) { await deleteLineItem(lineId); const supabase = getSupabaseServerClient(); const cartId = await getCartId(); const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount() + const account = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } - const { error } = await supabase - .schema('audit') - .from('cart_entries') - .insert({ - variant_id: lineId, - operation: 'REMOVE_FROM_CART', - account_id: account.id, - cart_id: cartId!, - changed_by: user.id, - }); + const { error } = await supabase.schema('audit').from('cart_entries').insert({ + variant_id: lineId, + operation: 'REMOVE_FROM_CART', + account_id: account.id, + cart_id: cartId!, + changed_by: user.id, + }); if (error) { throw new Error('Error logging cart entry: ' + error.message); } } -export async function handleNavigateToPayment({ language, paymentSessionId }: { language: string, paymentSessionId: string }) { +export async function handleNavigateToPayment({ + language, + paymentSessionId, +}: { + language: string; + paymentSessionId: string; +}) { const supabase = getSupabaseServerClient(); const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount() + const account = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } const cart = await retrieveCart(); if (!cart) { - throw new Error("No cart found"); + throw new Error('No cart found'); } - const paymentLink = await new MontonioOrderHandlerService().getMontonioPaymentLink({ - notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`, - returnUrl: `${env().siteUrl}/home/cart/montonio-callback`, - amount: cart.total, - currency: cart.currency_code.toUpperCase(), - description: `Order from Medreport`, - locale: language, - merchantReference: `${account.id}:${paymentSessionId}:${cart.id}`, - }); - - const { error } = await supabase - .schema('audit') - .from('cart_entries') - .insert({ - operation: 'NAVIGATE_TO_PAYMENT', - account_id: account.id, - cart_id: cart.id, - changed_by: user.id, + const paymentLink = + await new MontonioOrderHandlerService().getMontonioPaymentLink({ + notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`, + returnUrl: `${env().siteUrl}/home/cart/montonio-callback`, + amount: cart.total, + currency: cart.currency_code.toUpperCase(), + description: `Order from Medreport`, + locale: language, + merchantReference: `${account.id}:${paymentSessionId}:${cart.id}`, }); + + const { error } = await supabase.schema('audit').from('cart_entries').insert({ + operation: 'NAVIGATE_TO_PAYMENT', + account_id: account.id, + cart_id: cart.id, + changed_by: user.id, + }); if (error) { throw new Error('Error logging cart entry: ' + error.message); } @@ -137,26 +134,23 @@ export async function handleNavigateToPayment({ language, paymentSessionId }: { export async function handleLineItemTimeout({ lineItem, }: { - lineItem: StoreCartLineItem + lineItem: StoreCartLineItem; }) { const supabase = getSupabaseServerClient(); const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount() + const account = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } - + await deleteLineItem(lineItem.id); - const { error } = await supabase - .schema('audit') - .from('cart_entries') - .insert({ - operation: 'LINE_ITEM_TIMEOUT', - account_id: account.id, - cart_id: lineItem.cart_id, - changed_by: user.id, - }); + const { error } = await supabase.schema('audit').from('cart_entries').insert({ + operation: 'LINE_ITEM_TIMEOUT', + account_id: account.id, + cart_id: lineItem.cart_id, + changed_by: user.id, + }); if (error) { throw new Error('Error logging cart entry: ' + error.message); } 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), }); diff --git a/packages/email-templates/src/emails/doctor-summary-received.email.tsx b/packages/email-templates/src/emails/doctor-summary-received.email.tsx new file mode 100644 index 0000000..6ca01cd --- /dev/null +++ b/packages/email-templates/src/emails/doctor-summary-received.email.tsx @@ -0,0 +1,97 @@ +import { + Body, + Button, + Head, + Html, + Link, + Preview, + Tailwind, + Text, + render, +} from '@react-email/components'; + +import { BodyStyle } from '../components/body-style'; +import CommonFooter from '../components/common-footer'; +import { EmailContent } from '../components/content'; +import { EmailHeader } from '../components/header'; +import { EmailHeading } from '../components/heading'; +import { EmailWrapper } from '../components/wrapper'; +import { initializeEmailI18n } from '../lib/i18n'; + +export async function renderDoctorSummaryReceivedEmail({ + language, + recipientEmail, + recipientName, + orderNr, + orderId, +}: { + language?: string; + recipientName: string; + recipientEmail: string; + orderNr: string; + orderId: number; +}) { + const namespace = 'doctor-summary-received-email'; + + const { t } = await initializeEmailI18n({ + language, + namespace: [namespace, 'common'], + }); + + const to = recipientEmail; + + const previewText = t(`${namespace}:previewText`, { + orderNr, + }); + + const subject = t(`${namespace}:subject`, { + orderNr, + }); + + const html = await render( + + + + + + {previewText} + + + + + + {previewText} + + + + + {t(`${namespace}:hello`, { + displayName: recipientName, + })} + + + {t(`${namespace}:summaryReceivedForOrder`, { orderNr })} + + + + + + {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/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/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/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..5040c87 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 @@ -1022,7 +1088,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 +1107,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 +1126,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 +1147,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 +1159,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 +1171,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 +1850,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,12 +1862,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 +1901,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 +1919,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 +2011,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 +2135,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 +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" @@ -7959,6 +8023,7 @@ export type CompositeTypes< export const Constants = { audit: { Enums: { + action_status: ["SUCCESS", "FAIL"], doctor_page_view_action: [ "VIEW_ANALYSIS_RESULTS", "VIEW_DASHBOARD", @@ -7996,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/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": { 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; 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