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