From b7926f79a9c3c405d385aa2198a4632c673b31e9 Mon Sep 17 00:00:00 2001 From: Helena <37183360+helenarebane@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:18:18 +0300 Subject: [PATCH] MED-89: add analysis view with doctor summary (#68) * add analysis view with doctor summary * remove console.log, also return null if analysis data missing * replace orders table eye with button --- .../analysis-results/[id]/page.tsx | 107 ++++++++++++++ .../(dashboard)/analysis-results/page.tsx | 131 ------------------ .../_components/orders/order-items-table.tsx | 58 ++++---- .../(user)/_lib/server/load-user-analyses.ts | 22 +++ .../(user)/_lib/server/load-user-analysis.ts | 11 +- .../emails/doctor-summary-received.email.tsx | 8 +- packages/features/accounts/src/server/api.ts | 50 ++++++- .../features/accounts/src/types/accounts.ts | 50 +++++++ .../services/doctor-analysis.service.ts | 8 +- .../personal-account-navigation.config.tsx | 6 - public/locales/en/account.json | 3 +- public/locales/en/analysis-results.json | 3 +- public/locales/et/account.json | 3 +- public/locales/et/analysis-results.json | 3 +- 14 files changed, 284 insertions(+), 179 deletions(-) create mode 100644 app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx delete mode 100644 app/home/(user)/(dashboard)/analysis-results/page.tsx create mode 100644 app/home/(user)/_lib/server/load-user-analyses.ts diff --git a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx new file mode 100644 index 0000000..a568eed --- /dev/null +++ b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx @@ -0,0 +1,107 @@ +import Link from 'next/link'; + +import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip'; +import { pathsConfig } from '@kit/shared/config'; +import { Button } from '@kit/ui/button'; +import { PageBody, PageHeader } from '@kit/ui/page'; +import { Trans } from '@kit/ui/trans'; + +import { loadCurrentUserAccount } from '~/home/(user)/_lib/server/load-user-account'; +import { loadUserAnalysis } from '~/home/(user)/_lib/server/load-user-analysis'; +import { + PageViewAction, + createPageViewLog, +} from '~/lib/services/audit/pageView.service'; + +import Analysis from '../_components/analysis'; + +export default async function AnalysisResultsPage({ + params, +}: { + params: Promise<{ + id: string; + }>; +}) { + const account = await loadCurrentUserAccount(); + + const { id: analysisResponseId } = await params; + + const analysisResponse = await loadUserAnalysis(Number(analysisResponseId)); + + if (!account?.id || !analysisResponse) { + return null; + } + + await createPageViewLog({ + accountId: account.id, + action: PageViewAction.VIEW_ANALYSIS_RESULTS, + }); + + return ( + <> + + +
+
+

+ +

+

+ {analysisResponse?.elements && + analysisResponse.elements?.length > 0 ? ( + + ) : ( + + )} +

+
+ +
+
+

+ +

+
+ + +
+
+ {analysisResponse?.summary?.value && ( +
+ + + +

{analysisResponse.summary.value}

+
+ )} +
+ {analysisResponse.elements ? ( + analysisResponse.elements.map((element, index) => ( + + )) + ) : ( +
+ +
+ )} +
+
+ + ); +} diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx deleted file mode 100644 index 1dd999c..0000000 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import Link from 'next/link'; -import { redirect } from 'next/navigation'; - -import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account'; -import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; -import { withI18n } from '@/lib/i18n/with-i18n'; - -import { Trans } from '@kit/ui/makerkit/trans'; -import { PageBody } from '@kit/ui/page'; -import { Button } from '@kit/ui/shadcn/button'; - -import { pathsConfig } from '@kit/shared/config'; - -import { getAnalysisElements } from '~/lib/services/analysis-element.service'; -import { - PageViewAction, - createPageViewLog, -} from '~/lib/services/audit/pageView.service'; -import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service'; -import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip'; - -import { loadUserAnalysis } from '../../_lib/server/load-user-analysis'; -import Analysis from './_components/analysis'; - -export const generateMetadata = async () => { - const i18n = await createI18nServerInstance(); - const title = i18n.t('analysis-results:pageTitle'); - - return { - title, - }; -}; - -async function AnalysisResultsPage() { - const account = await loadCurrentUserAccount(); - if (!account) { - throw new Error('Account not found'); - } - - const analysisResponses = await loadUserAnalysis(); - const analysisResponseElements = analysisResponses?.flatMap( - ({ elements }) => elements, - ); - - const analysisOrders = await getAnalysisOrders().catch(() => null); - - if (!analysisOrders) { - redirect(pathsConfig.auth.signIn); - } - - await createPageViewLog({ - accountId: account.id, - action: PageViewAction.VIEW_ANALYSIS_RESULTS, - }); - - const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [ - ...new Set(analysisOrders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]), - ]; - - const analysisElementIds = getAnalysisElementIds(analysisOrders); - const analysisElements = await getAnalysisElements({ ids: analysisElementIds }); - - return ( - -
-
-

- -

-

- {analysisResponses && analysisResponses.length > 0 ? ( - - ) : ( - - )} -

-
- -
-
- {analysisOrders.length > 0 && analysisElements.length > 0 ? analysisOrders.map((analysisOrder) => { - const analysisResponse = analysisResponses?.find((response) => response.analysis_order_id === analysisOrder.id); - const analysisElementIds = getAnalysisElementIds([analysisOrder]); - const analysisElementsForOrder = analysisElements.filter((element) => analysisElementIds.includes(element.id)); - return ( -
-

- -

-
- - -
-
- {analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => { - const results = analysisResponse?.elements.some((element) => element.analysis_element_original_id === analysisElement.analysis_id_original) - && analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original); - if (!results) { - return ( - - ); - } - return ( - - ); - }) : ( -
- -
- )} -
-
- ); - }) : ( -
- -
- )} -
-
- ); -} - -export default withI18n(AnalysisResultsPage); diff --git a/app/home/(user)/_components/orders/order-items-table.tsx b/app/home/(user)/_components/orders/order-items-table.tsx index 096ad06..b1e4852 100644 --- a/app/home/(user)/_components/orders/order-items-table.tsx +++ b/app/home/(user)/_components/orders/order-items-table.tsx @@ -1,22 +1,32 @@ 'use client'; -import { Trans } from '@kit/ui/trans'; +import { useRouter } from 'next/navigation'; + +import { StoreOrderLineItem } from '@medusajs/types'; +import { formatDate } from 'date-fns'; +import { Eye } from 'lucide-react'; + +import { pathsConfig } from '@kit/shared/config'; +import { Button } from '@kit/ui/button'; import { Table, TableBody, - TableHead, - TableRow, - TableHeader, TableCell, + TableHead, + TableHeader, + TableRow, } from '@kit/ui/table'; -import { StoreOrderLineItem } from "@medusajs/types"; +import { Trans } from '@kit/ui/trans'; + import { AnalysisOrder } from '~/lib/services/order.service'; -import { formatDate } from 'date-fns'; -import { Eye } from 'lucide-react'; -import { useRouter } from 'next/navigation'; + import { logAnalysisResultsNavigateAction } from './actions'; -export default function OrderItemsTable({ items, title, analysisOrder }: { +export default function OrderItemsTable({ + items, + title, + analysisOrder, +}: { items: StoreOrderLineItem[]; title: string; analysisOrder: AnalysisOrder; @@ -29,11 +39,11 @@ export default function OrderItemsTable({ items, title, analysisOrder }: { const openAnalysisResults = async () => { await logAnalysisResultsNavigateAction(analysisOrder.medusa_order_id); - router.push(`/home/analysis-results`); - } + router.push(`${pathsConfig.app.analysisResults}/${analysisOrder.id}`); + }; return ( - +
@@ -45,13 +55,14 @@ export default function OrderItemsTable({ items, title, analysisOrder }: { - - + {items - .sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1) + .sort((a, b) => + (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1, + ) .map((orderItem) => ( @@ -64,23 +75,18 @@ export default function OrderItemsTable({ items, title, analysisOrder }: { {formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')} - + - - - - + + ))}
- ) + ); } diff --git a/app/home/(user)/_lib/server/load-user-analyses.ts b/app/home/(user)/_lib/server/load-user-analyses.ts new file mode 100644 index 0000000..388bfec --- /dev/null +++ b/app/home/(user)/_lib/server/load-user-analyses.ts @@ -0,0 +1,22 @@ +import { cache } from 'react'; + +import { createAccountsApi } from '@kit/accounts/api'; +import { UserAnalysis } from '@kit/accounts/types/accounts'; +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + +export type UserAnalyses = Awaited>; + +/** + * @name loadUserAnalyses + * @description + * Load the user's analyses. It's a cached per-request function that fetches the user workspace data. + * It can be used across the server components to load the user workspace data. + */ +export const loadUserAnalyses = cache(analysesLoader); + +async function analysesLoader(): Promise { + const client = getSupabaseServerClient(); + const api = createAccountsApi(client); + + return api.getUserAnalyses(); +} diff --git a/app/home/(user)/_lib/server/load-user-analysis.ts b/app/home/(user)/_lib/server/load-user-analysis.ts index 52cd529..09efd46 100644 --- a/app/home/(user)/_lib/server/load-user-analysis.ts +++ b/app/home/(user)/_lib/server/load-user-analysis.ts @@ -1,7 +1,7 @@ import { cache } from 'react'; import { createAccountsApi } from '@kit/accounts/api'; -import { UserAnalysis } from '@kit/accounts/types/accounts'; +import { AnalysisResultDetails } from '@kit/accounts/types/accounts'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; export type UserAnalyses = Awaited>; @@ -9,14 +9,15 @@ export type UserAnalyses = Awaited>; /** * @name loadUserAnalysis * @description - * Load the user's analyses. It's a cached per-request function that fetches the user workspace data. - * It can be used across the server components to load the user workspace data. + * Load the user's analysis based on id. It's a cached per-request function that fetches the user's analysis data. */ export const loadUserAnalysis = cache(analysisLoader); -async function analysisLoader(): Promise { +async function analysisLoader( + analysisOrderId: number, +): Promise { const client = getSupabaseServerClient(); const api = createAccountsApi(client); - return api.getUserAnalysis(); + return api.getUserAnalysis(analysisOrderId); } diff --git a/packages/email-templates/src/emails/doctor-summary-received.email.tsx b/packages/email-templates/src/emails/doctor-summary-received.email.tsx index d091160..69ce37e 100644 --- a/packages/email-templates/src/emails/doctor-summary-received.email.tsx +++ b/packages/email-templates/src/emails/doctor-summary-received.email.tsx @@ -21,12 +21,12 @@ export async function renderDoctorSummaryReceivedEmail({ language, recipientName, orderNr, - orderId, + analysisOrderId, }: { language?: string; recipientName: string; orderNr: string; - orderId: number; + analysisOrderId: number; }) { const namespace = 'doctor-summary-received-email'; @@ -69,13 +69,13 @@ export async function renderDoctorSummaryReceivedEmail({ {t(`${namespace}:linkText`, { orderNr })} {t(`${namespace}:ifButtonDisabled`)}{' '} - {`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`} + {`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`} diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index 0844138..336797d 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -2,7 +2,11 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; -import { UserAnalysis } from '../types/accounts'; +import { + AnalysisResultDetails, + UserAnalysis, + UserAnalysisResponse, +} from '../types/accounts'; export type AccountWithParams = Database['medreport']['Tables']['accounts']['Row'] & { @@ -184,7 +188,49 @@ class AccountsApi { return response.data?.customer_id; } - async getUserAnalysis(): Promise { + async getUserAnalysis( + analysisOrderId: number, + ): Promise { + const authUser = await this.client.auth.getUser(); + const { data, error: userError } = authUser; + + if (userError) { + console.error('Failed to get user', userError); + throw userError; + } + + const { user } = data; + + const { data: analysisResponse } = await this.client + .schema('medreport') + .from('analysis_responses') + .select( + `*, + elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time), + order:analysis_order_id(medusa_order_id, status, created_at), + summary:analysis_order_id(doctor_analysis_feedback(*))`, + ) + .eq('user_id', user.id) + .eq('analysis_order_id', analysisOrderId) + .throwOnError(); + + const responseWithElements = analysisResponse?.[0]; + if (!responseWithElements) { + return null; + } + + const feedback = responseWithElements.summary.doctor_analysis_feedback?.[0]; + + return { + ...responseWithElements, + summary: + feedback?.status === 'COMPLETED' + ? responseWithElements.summary.doctor_analysis_feedback?.[0] + : null, + }; + } + + async getUserAnalyses(): Promise { const authUser = await this.client.auth.getUser(); const { data, error: userError } = authUser; diff --git a/packages/features/accounts/src/types/accounts.ts b/packages/features/accounts/src/types/accounts.ts index 2d54306..86d51f3 100644 --- a/packages/features/accounts/src/types/accounts.ts +++ b/packages/features/accounts/src/types/accounts.ts @@ -1,3 +1,5 @@ +import * as z from 'zod'; + import { Database } from '@kit/supabase/database'; export type UserAnalysisElement = @@ -15,3 +17,51 @@ export enum ApplicationRoleEnum { Doctor = 'doctor', SuperAdmin = 'super_admin', } + +export const ElementSchema = z.object({ + unit: z.string(), + norm_lower: z.number(), + norm_upper: z.number(), + norm_status: z.number(), + analysis_name: z.string(), + response_time: z.string(), + response_value: z.number(), + norm_lower_included: z.boolean(), + norm_upper_included: z.boolean(), +}); +export type Element = z.infer; + +export const OrderSchema = z.object({ + status: z.string(), + medusa_order_id: z.string(), + created_at: z.coerce.date(), +}); +export type Order = z.infer; + +export const SummarySchema = z.object({ + id: z.number(), + value: z.string(), + status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + created_by: z.string(), + updated_at: z.coerce.date().nullable(), + updated_by: z.string(), + doctor_user_id: z.string().nullable(), + analysis_order_id: z.number(), +}); +export type Summary = z.infer; + +export const AnalysisResultDetailsSchema = z.object({ + id: z.number(), + analysis_order_id: z.number(), + order_number: z.string(), + order_status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + updated_at: z.coerce.date().nullable(), + elements: z.array(ElementSchema), + order: OrderSchema, + summary: SummarySchema.nullable(), +}); +export type AnalysisResultDetails = z.infer; 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 4f30fd8..95790ba 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 @@ -656,6 +656,12 @@ export async function submitFeedback( .eq('id', analysisOrderId) .limit(1) .throwOnError(), + supabase + .schema('medreport') + .from('analysis_orders') + .update({ status: 'COMPLETED' }) + .eq('id', analysisOrderId) + .throwOnError(), ]); if (!recipient?.[0]?.email) { @@ -674,7 +680,7 @@ export async function submitFeedback( language: preferred_locale ?? 'et', recipientName: getFullName(name, last_name), orderNr: analysisOrder?.[0]?.medusa_order_id ?? '', - orderId: analysisOrder[0].id, + analysisOrderId: analysisOrder[0].id, }, email, ); diff --git a/packages/shared/src/config/personal-account-navigation.config.tsx b/packages/shared/src/config/personal-account-navigation.config.tsx index 521ba0a..ea5f2da 100644 --- a/packages/shared/src/config/personal-account-navigation.config.tsx +++ b/packages/shared/src/config/personal-account-navigation.config.tsx @@ -35,12 +35,6 @@ const routes = [ Icon: , end: true, }, - { - label: 'common:routes.analysisResults', - path: pathsConfig.app.analysisResults, - Icon: , - end: true, - }, { label: 'common:routes.orderAnalysisPackage', path: pathsConfig.app.orderAnalysisPackage, diff --git a/public/locales/en/account.json b/public/locales/en/account.json index eab22ac..1a324fc 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -128,5 +128,6 @@ "updateRoleLoading": "Updating role...", "updatePreferredLocaleSuccess": "Language preference updated", "updatePreferredLocaleError": "Language preference update failed", - "updatePreferredLocaleLoading": "Updating language preference..." + "updatePreferredLocaleLoading": "Updating language preference...", + "doctorAnalysisSummary": "Doctor's summary" } \ No newline at end of file diff --git a/public/locales/en/analysis-results.json b/public/locales/en/analysis-results.json index 9f6a491..571b1fb 100644 --- a/public/locales/en/analysis-results.json +++ b/public/locales/en/analysis-results.json @@ -12,5 +12,6 @@ "normal": "Normal range" } }, - "orderTitle": "Order number {{orderNumber}}" + "orderTitle": "Order number {{orderNumber}}", + "view": "View results" } \ No newline at end of file diff --git a/public/locales/et/account.json b/public/locales/et/account.json index 7c6e5cd..b268146 100644 --- a/public/locales/et/account.json +++ b/public/locales/et/account.json @@ -151,5 +151,6 @@ "updateRoleLoading": "Rolli uuendatakse...", "updatePreferredLocaleSuccess": "Eelistatud keel uuendatud", "updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud", - "updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse..." + "updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse...", + "doctorAnalysisSummary": "Arsti kokkuvõte analüüsitulemuste kohta" } \ No newline at end of file diff --git a/public/locales/et/analysis-results.json b/public/locales/et/analysis-results.json index 6334d60..9efe9bd 100644 --- a/public/locales/et/analysis-results.json +++ b/public/locales/et/analysis-results.json @@ -12,5 +12,6 @@ "normal": "Normaalne vahemik" } }, - "orderTitle": "Tellimus {{orderNumber}}" + "orderTitle": "Tellimus {{orderNumber}}", + "view": "Vaata tulemusi" } \ No newline at end of file