From 49fc75b17be29a1e1a338cc09e4506d87de7e044 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 11 Aug 2025 09:21:06 +0300 Subject: [PATCH 1/8] feat(MED-105): update analysis results page --- .../_components/analysis-level-bar.tsx | 12 ++- .../analysis-results/_components/analysis.tsx | 85 +++++++++++-------- .../(dashboard)/analysis-results/page.tsx | 74 ++++++++++------ lib/i18n/i18n.settings.ts | 1 + lib/services/analysis-element.service.ts | 9 +- .../features/accounts/src/types/accounts.ts | 9 +- packages/ui/src/makerkit/page.tsx | 2 +- public/locales/en/account.json | 6 -- public/locales/en/analysis-results.json | 12 +++ public/locales/et/account.json | 6 -- public/locales/et/analysis-results.json | 12 +++ styles/theme.css | 4 +- utils/medusa-product.ts | 2 +- 13 files changed, 147 insertions(+), 87 deletions(-) create mode 100644 public/locales/en/analysis-results.json create mode 100644 public/locales/et/analysis-results.json diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx index 9b23701..6714f80 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx @@ -19,7 +19,7 @@ const Level = ({ isLast = false, }: { isActive?: boolean; - color: 'destructive' | 'success' | 'warning'; + color: 'destructive' | 'success' | 'warning' | 'gray-200'; isFirst?: boolean; isLast?: boolean; }) => { @@ -40,6 +40,14 @@ const Level = ({ ); }; +export const AnalysisLevelBarSkeleton = () => { + return ( +
+ +
+ ); +}; + const AnalysisLevelBar = ({ normLowerIncluded = true, normUpperIncluded = true, @@ -50,7 +58,7 @@ const AnalysisLevelBar = ({ level: AnalysisResultLevel; }) => { return ( -
+
{normLowerIncluded && ( <> { + const name = analysisElement.analysis_name_lab || ''; + const status = results?.norm_status || AnalysisStatus.NORMAL; + const value = results?.response_value || 0; + const unit = results?.unit || ''; + const normLowerIncluded = results?.norm_lower_included || false; + const normUpperIncluded = results?.norm_upper_included || false; + const normLower = results?.norm_lower || 0; + const normUpper = results?.norm_upper || 0; + const [showTooltip, setShowTooltip] = useState(false); const isUnderNorm = value < normLower; const getAnalysisResultLevel = () => { + if (!results) { + return null; + } if (isUnderNorm) { switch (status) { case AnalysisStatus.MEDIUM: @@ -59,7 +58,7 @@ const Analysis = ({ }; return ( -
+
{name}
-
-
{value}
-
{unit}
-
-
- {normLower} - {normUpper} -
Normaalne vahemik
-
- + {results ? ( + <> +
+
{value}
+
{unit}
+
+
+ {normLower} - {normUpper} +
+ +
+
+ + + ) : ( + <> +
+
+ +
+
+
+ + + )}
); }; diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx index 0d3687c..4060560 100644 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx @@ -1,4 +1,5 @@ -import { Fragment } from 'react'; +import Link from 'next/link'; + import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { withI18n } from '@/lib/i18n/with-i18n'; @@ -7,11 +8,17 @@ import { PageBody } from '@kit/ui/page'; import { Button } from '@kit/ui/shadcn/button'; import { loadUserAnalysis } from '../../_lib/server/load-user-analysis'; -import Analysis, { AnalysisStatus } from './_components/analysis'; +import Analysis from './_components/analysis'; +import { listProductTypes } from '@lib/data/products'; +import pathsConfig from '~/config/paths.config'; +import { redirect } from 'next/navigation'; +import { getOrders } from '~/lib/services/order.service'; +import { AnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service'; +import type { UserAnalysisElement } from '@kit/accounts/types/accounts'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); - const title = i18n.t('account:analysisResults.pageTitle'); + const title = i18n.t('analysis-results:pageTitle'); return { title, @@ -21,45 +28,56 @@ export const generateMetadata = async () => { async function AnalysisResultsPage() { const analysisList = await loadUserAnalysis(); + const orders = await getOrders().catch(() => null); + const { productTypes } = await listProductTypes(); + + if (!orders || !productTypes) { + redirect(pathsConfig.auth.signIn); + } + + const analysisElementIds = [ + ...new Set(orders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]), + ]; + const analysisElements = await getAnalysisElements({ ids: analysisElementIds }); + const analysisElementsWithResults = analysisElements.reduce((acc, curr) => { + const analysisResponseWithElementResults = analysisList?.find((result) => result.elements.some((element) => element.analysis_element_original_id === curr.analysis_id_original)); + const elementResults = analysisResponseWithElementResults?.elements.find((element) => element.analysis_element_original_id === curr.analysis_id_original) as UserAnalysisElement | undefined; + return { + ...acc, + [curr.id]: { + analysisElement: curr, + results: elementResults, + }, + } + }, {} as Record); + return ( -
+

- +

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

-
- {analysisList?.map((analysis) => ( - - {analysis.elements.map((element) => ( - - ))} - - ))} + {Object.entries(analysisElementsWithResults).map(([id, { analysisElement, results }]) => { + return ( + + ); + })}
); diff --git a/lib/i18n/i18n.settings.ts b/lib/i18n/i18n.settings.ts index 2d9dad2..e41095f 100644 --- a/lib/i18n/i18n.settings.ts +++ b/lib/i18n/i18n.settings.ts @@ -39,6 +39,7 @@ export const defaultI18nNamespaces = [ 'order-analysis', 'cart', 'orders', + 'analysis-results', ]; /** diff --git a/lib/services/analysis-element.service.ts b/lib/services/analysis-element.service.ts index 2777a77..635320c 100644 --- a/lib/services/analysis-element.service.ts +++ b/lib/services/analysis-element.service.ts @@ -1,4 +1,3 @@ -import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { Json, Tables } from '@kit/supabase/database'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import type { IMaterialGroup, IUuringElement } from './medipost.types'; @@ -9,10 +8,12 @@ export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements export async function getAnalysisElements({ originalIds, + ids, }: { originalIds?: string[]; + ids?: number[]; }): Promise { - const query = getSupabaseServerClient() + const query = getSupabaseServerAdminClient() .schema('medreport') .from('analysis_elements') .select(`*, analysis_groups(*)`) @@ -22,6 +23,10 @@ export async function getAnalysisElements({ query.in('analysis_id_original', [...new Set(originalIds)]); } + if (Array.isArray(ids)) { + query.in('id', ids); + } + const { data: analysisElements, error } = await query; if (error) { diff --git a/packages/features/accounts/src/types/accounts.ts b/packages/features/accounts/src/types/accounts.ts index 8048dea..bbc4ddb 100644 --- a/packages/features/accounts/src/types/accounts.ts +++ b/packages/features/accounts/src/types/accounts.ts @@ -1,6 +1,7 @@ import { Database } from '@kit/supabase/database'; -export type UserAnalysis = - (Database['medreport']['Tables']['analysis_responses']['Row'] & { - elements: Database['medreport']['Tables']['analysis_response_elements']['Row'][]; - })[]; +export type UserAnalysisElement = Database['medreport']['Tables']['analysis_response_elements']['Row']; +export type UserAnalysisResponse = Database['medreport']['Tables']['analysis_responses']['Row'] & { + elements: UserAnalysisElement[]; +}; +export type UserAnalysis = UserAnalysisResponse[]; diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index 4f82a7b..e089171 100644 --- a/packages/ui/src/makerkit/page.tsx +++ b/packages/ui/src/makerkit/page.tsx @@ -42,7 +42,7 @@ function PageWithSidebar(props: PageProps) { > {MobileNavigation} -
+
{Children}
diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 95f7e2f..edbf616 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -122,11 +122,5 @@ "consentToAnonymizedCompanyData": { "label": "Consent to be included in employer statistics", "description": "Consent to be included in anonymized company statistics" - }, - "analysisResults": { - "pageTitle": "My analysis results", - "description": "Super, you've already done your analysis. Here are your important results:", - "descriptionEmpty": "If you've already done your analysis, your results will appear here soon.", - "orderNewAnalysis": "Order new analyses" } } diff --git a/public/locales/en/analysis-results.json b/public/locales/en/analysis-results.json new file mode 100644 index 0000000..3d53e6b --- /dev/null +++ b/public/locales/en/analysis-results.json @@ -0,0 +1,12 @@ +{ + "pageTitle": "My analysis results", + "description": "All analysis results will appear here within 1-3 business days after they have been done.", + "descriptionEmpty": "If you've already done your analysis, your results will appear here soon.", + "orderNewAnalysis": "Order new analyses", + "waitingForResults": "Waiting for results", + "results": { + "range": { + "normal": "Normal range" + } + } +} \ No newline at end of file diff --git a/public/locales/et/account.json b/public/locales/et/account.json index 836c5e4..ade6520 100644 --- a/public/locales/et/account.json +++ b/public/locales/et/account.json @@ -145,11 +145,5 @@ "successTitle": "Tere, {{firstName}} {{lastName}}", "successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!", "successButton": "Jätka" - }, - "analysisResults": { - "pageTitle": "Minu analüüside vastused", - "description": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad:", - "descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.", - "orderNewAnalysis": "Telli uued analüüsid" } } diff --git a/public/locales/et/analysis-results.json b/public/locales/et/analysis-results.json new file mode 100644 index 0000000..59cfcaa --- /dev/null +++ b/public/locales/et/analysis-results.json @@ -0,0 +1,12 @@ +{ + "pageTitle": "Minu analüüside vastused", + "description": "Kõikide analüüside tulemused ilmuvad 1-3 tööpäeva jooksul peale nende andmist.", + "descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.", + "orderNewAnalysis": "Telli uued analüüsid", + "waitingForResults": "Tulemuse ootel", + "results": { + "range": { + "normal": "Normaalne vahemik" + } + } +} \ No newline at end of file diff --git a/styles/theme.css b/styles/theme.css index 2b766a7..46641c3 100644 --- a/styles/theme.css +++ b/styles/theme.css @@ -135,8 +135,8 @@ --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; - --breakpoint-xs: 30rem; - --breakpoint-sm: 48rem; + --breakpoint-xs: 48rem; + --breakpoint-sm: 64rem; --breakpoint-md: 70rem; --breakpoint-lg: 80rem; --breakpoint-xl: 96rem; diff --git a/utils/medusa-product.ts b/utils/medusa-product.ts index a93669b..ef4384f 100644 --- a/utils/medusa-product.ts +++ b/utils/medusa-product.ts @@ -5,7 +5,7 @@ export const getAnalysisElementMedusaProductIds = (products: ({ metadata?: { ana const mapped = products .flatMap((product) => { - const value = product?.metadata?.analysisElementMedusaProductIds; + const value = product?.metadata?.analysisElementMedusaProductIds?.replaceAll("'", '"'); try { return JSON.parse(value as string); } catch (e) { From 83fff1ffe7576f7cb3c28bb50e864c37de895f9b Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 11 Aug 2025 09:21:13 +0300 Subject: [PATCH 2/8] feat(MED-105): create audit entry on analysis results view --- .../(dashboard)/analysis-results/page.tsx | 12 +++++++ lib/services/audit/pageView.service.ts | 35 +++++++++++++++++++ packages/supabase/src/database.types.ts | 24 +++++++++++++ .../20250811065135_audit_page_views.sql | 24 +++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 lib/services/audit/pageView.service.ts create mode 100644 supabase/migrations/20250811065135_audit_page_views.sql diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx index 4060560..7b8a97e 100644 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx @@ -15,6 +15,8 @@ import { redirect } from 'next/navigation'; import { getOrders } from '~/lib/services/order.service'; import { AnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service'; import type { UserAnalysisElement } from '@kit/accounts/types/accounts'; +import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account'; +import { createPageViewLog } from '~/lib/services/audit/pageView.service'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -26,6 +28,11 @@ export const generateMetadata = async () => { }; async function AnalysisResultsPage() { + const account = await loadCurrentUserAccount() + if (!account) { + throw new Error('Account not found'); + } + const analysisList = await loadUserAnalysis(); const orders = await getOrders().catch(() => null); @@ -35,6 +42,11 @@ async function AnalysisResultsPage() { redirect(pathsConfig.auth.signIn); } + await createPageViewLog({ + accountId: account.id, + action: 'VIEW_ANALYSIS_RESULTS', + }); + const analysisElementIds = [ ...new Set(orders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]), ]; diff --git a/lib/services/audit/pageView.service.ts b/lib/services/audit/pageView.service.ts new file mode 100644 index 0000000..719db6d --- /dev/null +++ b/lib/services/audit/pageView.service.ts @@ -0,0 +1,35 @@ +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + +export const createPageViewLog = async ({ + accountId, + action, +}: { + accountId: string; + action: 'VIEW_ANALYSIS_RESULTS'; +}) => { + try { + const supabase = getSupabaseServerClient(); + + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser(); + + if (userError || !user) { + console.error('No authenticated user found; skipping audit insert'); + return; + } + + await supabase + .schema('audit') + .from('page_views') + .insert({ + account_id: accountId, + action, + changed_by: user.id, + }) + .throwOnError(); + } catch (error) { + console.error('Failed to insert page view log', error); + } +} diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 94e7d39..f7316cc 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -81,6 +81,30 @@ export type Database = { } Relationships: [] } + page_views: { + Row: { + account_id: string + action: string + changed_by: string + created_at: string + id: number + } + Insert: { + account_id: string + action: string + changed_by: string + created_at?: string + id?: number + } + Update: { + account_id?: string + action: string + changed_by?: string + created_at?: string + id?: number + } + Relationships: [] + } request_entries: { Row: { comment: string | null diff --git a/supabase/migrations/20250811065135_audit_page_views.sql b/supabase/migrations/20250811065135_audit_page_views.sql new file mode 100644 index 0000000..ceab355 --- /dev/null +++ b/supabase/migrations/20250811065135_audit_page_views.sql @@ -0,0 +1,24 @@ +create table "audit"."page_views" ( + "id" bigint generated by default as identity not null, + "account_id" text not null, + "action" text not null, + "created_at" timestamp with time zone not null default now(), + "changed_by" uuid not null +); + +grant usage on schema audit to authenticated; +grant select, insert, update, delete on table audit.page_views to authenticated; + +alter table "audit"."page_views" enable row level security; + +create policy "insert_own" +on "audit"."page_views" +as permissive +for insert +to authenticated +with check (auth.uid() = changed_by); + +create policy "service_role_select" on "audit"."page_views" for select to service_role using (true); +create policy "service_role_insert" on "audit"."page_views" for insert to service_role with check (true); +create policy "service_role_update" on "audit"."page_views" for update to service_role using (true); +create policy "service_role_delete" on "audit"."page_views" for delete to service_role using (true); From c8621c4453b17e11679250292be3d1218f614500 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 11 Aug 2025 09:21:20 +0300 Subject: [PATCH 3/8] feat(MED-105): sort analysis result elements by date --- .../(dashboard)/analysis-results/page.tsx | 50 +++++++++++-------- app/home/(user)/(dashboard)/order/page.tsx | 4 +- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx index 7b8a97e..efd68e1 100644 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx @@ -9,12 +9,10 @@ import { Button } from '@kit/ui/shadcn/button'; import { loadUserAnalysis } from '../../_lib/server/load-user-analysis'; import Analysis from './_components/analysis'; -import { listProductTypes } from '@lib/data/products'; import pathsConfig from '~/config/paths.config'; import { redirect } from 'next/navigation'; import { getOrders } from '~/lib/services/order.service'; -import { AnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service'; -import type { UserAnalysisElement } from '@kit/accounts/types/accounts'; +import { getAnalysisElements } from '~/lib/services/analysis-element.service'; import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account'; import { createPageViewLog } from '~/lib/services/audit/pageView.service'; @@ -33,12 +31,12 @@ async function AnalysisResultsPage() { throw new Error('Account not found'); } - const analysisList = await loadUserAnalysis(); + const analysisResponses = await loadUserAnalysis(); + const analysisResponseElements = analysisResponses?.flatMap(({ elements }) => elements); const orders = await getOrders().catch(() => null); - const { productTypes } = await listProductTypes(); - if (!orders || !productTypes) { + if (!orders) { redirect(pathsConfig.auth.signIn); } @@ -51,17 +49,20 @@ async function AnalysisResultsPage() { ...new Set(orders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]), ]; const analysisElements = await getAnalysisElements({ ids: analysisElementIds }); - const analysisElementsWithResults = analysisElements.reduce((acc, curr) => { - const analysisResponseWithElementResults = analysisList?.find((result) => result.elements.some((element) => element.analysis_element_original_id === curr.analysis_id_original)); - const elementResults = analysisResponseWithElementResults?.elements.find((element) => element.analysis_element_original_id === curr.analysis_id_original) as UserAnalysisElement | undefined; - return { - ...acc, - [curr.id]: { - analysisElement: curr, - results: elementResults, - }, - } - }, {} as Record); + const analysisElementsWithResults = analysisResponseElements + ?.sort((a, b) => { + if (!a.updated_at || !b.updated_at) { + return 0; + } + return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); + }) + .map((results) => { + return { + results, + } + }); + const analysisElementsWithoutResults = analysisElements + .filter((element) => !analysisElementsWithResults?.some(({ results }) => results.analysis_element_original_id === element.analysis_id_original)); return ( @@ -71,7 +72,7 @@ async function AnalysisResultsPage() {

- {analysisList && analysisList.length > 0 ? ( + {analysisResponses && analysisResponses.length > 0 ? ( ) : ( @@ -85,9 +86,18 @@ async function AnalysisResultsPage() {

- {Object.entries(analysisElementsWithResults).map(([id, { analysisElement, results }]) => { + {analysisElementsWithResults?.map(({ results }) => { + const analysisElement = analysisElements.find((element) => element.analysis_id_original === results.analysis_element_original_id); + if (!analysisElement) { + return null; + } return ( - + + ); + })} + {analysisElementsWithoutResults?.map((element) => { + return ( + ); })}
diff --git a/app/home/(user)/(dashboard)/order/page.tsx b/app/home/(user)/(dashboard)/order/page.tsx index 98c1c4f..97144af 100644 --- a/app/home/(user)/(dashboard)/order/page.tsx +++ b/app/home/(user)/(dashboard)/order/page.tsx @@ -3,7 +3,6 @@ import { redirect } from 'next/navigation'; import { listOrders } from '~/medusa/lib/data/orders'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { listProductTypes } from '@lib/data/products'; -import { retrieveCustomer } from '@lib/data/customer'; import { PageBody } from '@kit/ui/makerkit/page'; import pathsConfig from '~/config/paths.config'; import { Trans } from '@kit/ui/trans'; @@ -21,11 +20,10 @@ export async function generateMetadata() { } async function OrdersPage() { - const customer = await retrieveCustomer(); const orders = await listOrders().catch(() => null); const { productTypes } = await listProductTypes(); - if (!customer || !orders || !productTypes) { + if (!orders || !productTypes) { redirect(pathsConfig.auth.signIn); } From 37f233e36308800a96f92fc1011ecab1fe54fac3 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 11 Aug 2025 09:21:27 +0300 Subject: [PATCH 4/8] feat(MED-105): show empty text on no ordered analyses --- .../(dashboard)/analysis-results/page.tsx | 21 ++++++++++++------- public/locales/en/analysis-results.json | 1 + public/locales/et/analysis-results.json | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx index efd68e1..d5b5293 100644 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx @@ -60,10 +60,12 @@ async function AnalysisResultsPage() { return { results, } - }); + }) ?? []; const analysisElementsWithoutResults = analysisElements .filter((element) => !analysisElementsWithResults?.some(({ results }) => results.analysis_element_original_id === element.analysis_id_original)); + const hasNoAnalysisElements = analysisElementsWithResults.length === 0 && analysisElementsWithoutResults.length === 0; + return (
@@ -80,13 +82,13 @@ async function AnalysisResultsPage() {

- {analysisElementsWithResults?.map(({ results }) => { + {analysisElementsWithResults.map(({ results }) => { const analysisElement = analysisElements.find((element) => element.analysis_id_original === results.analysis_element_original_id); if (!analysisElement) { return null; @@ -95,11 +97,14 @@ async function AnalysisResultsPage() { ); })} - {analysisElementsWithoutResults?.map((element) => { - return ( - - ); - })} + {analysisElementsWithoutResults.map((element) => ( + + ))} + {hasNoAnalysisElements && ( +
+ +
+ )}
); diff --git a/public/locales/en/analysis-results.json b/public/locales/en/analysis-results.json index 3d53e6b..8dd0a09 100644 --- a/public/locales/en/analysis-results.json +++ b/public/locales/en/analysis-results.json @@ -4,6 +4,7 @@ "descriptionEmpty": "If you've already done your analysis, your results will appear here soon.", "orderNewAnalysis": "Order new analyses", "waitingForResults": "Waiting for results", + "noAnalysisElements": "No analysis orders found", "results": { "range": { "normal": "Normal range" diff --git a/public/locales/et/analysis-results.json b/public/locales/et/analysis-results.json index 59cfcaa..f8a7c2d 100644 --- a/public/locales/et/analysis-results.json +++ b/public/locales/et/analysis-results.json @@ -4,6 +4,7 @@ "descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.", "orderNewAnalysis": "Telli uued analüüsid", "waitingForResults": "Tulemuse ootel", + "noAnalysisElements": "Veel ei ole tellitud analüüse", "results": { "range": { "normal": "Normaalne vahemik" From 556d7bd3217985087b9cad5e0db2d90e402acb28 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 11 Aug 2025 09:21:33 +0300 Subject: [PATCH 5/8] feat(MED-105): show analysis date in tooltip for now --- .../analysis-results/_components/analysis.tsx | 27 ++++++++++--------- .../(dashboard)/analysis-results/page.tsx | 10 +++---- public/locales/en/analysis-results.json | 1 + public/locales/et/analysis-results.json | 1 + 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx index 4f3fddb..95f042d 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useState } from 'react'; +import { format } from 'date-fns'; import { Info } from 'lucide-react'; @@ -61,21 +62,23 @@ const Analysis = ({
{name} -
setShowTooltip(!showTooltip)} - onMouseLeave={() => setShowTooltip(false)} - > - {' '} + {results?.response_time && ( -
+ )}
{results ? ( <> diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx index d5b5293..fcc7c9e 100644 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx @@ -51,16 +51,12 @@ async function AnalysisResultsPage() { const analysisElements = await getAnalysisElements({ ids: analysisElementIds }); const analysisElementsWithResults = analysisResponseElements ?.sort((a, b) => { - if (!a.updated_at || !b.updated_at) { + if (!a.response_time || !b.response_time) { return 0; } - return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); + return new Date(b.response_time).getTime() - new Date(a.response_time).getTime(); }) - .map((results) => { - return { - results, - } - }) ?? []; + .map((results) => ({ results })) ?? []; const analysisElementsWithoutResults = analysisElements .filter((element) => !analysisElementsWithResults?.some(({ results }) => results.analysis_element_original_id === element.analysis_id_original)); diff --git a/public/locales/en/analysis-results.json b/public/locales/en/analysis-results.json index 8dd0a09..ada8eac 100644 --- a/public/locales/en/analysis-results.json +++ b/public/locales/en/analysis-results.json @@ -5,6 +5,7 @@ "orderNewAnalysis": "Order new analyses", "waitingForResults": "Waiting for results", "noAnalysisElements": "No analysis orders found", + "analysisDate": "Analysis result date", "results": { "range": { "normal": "Normal range" diff --git a/public/locales/et/analysis-results.json b/public/locales/et/analysis-results.json index f8a7c2d..0acc140 100644 --- a/public/locales/et/analysis-results.json +++ b/public/locales/et/analysis-results.json @@ -5,6 +5,7 @@ "orderNewAnalysis": "Telli uued analüüsid", "waitingForResults": "Tulemuse ootel", "noAnalysisElements": "Veel ei ole tellitud analüüse", + "analysisDate": "Analüüsi vastuse kuupäev", "results": { "range": { "normal": "Normaalne vahemik" From d582e222ce5fd1fe58de223a83e464ec634a2d90 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 11 Aug 2025 09:21:40 +0300 Subject: [PATCH 6/8] feat(MED-105): update order details redirect and shown page --- .../cart/montonio-callback/actions.ts | 4 +- .../montonio-callback/client-component.tsx | 4 +- .../cart/montonio-callback/page.tsx | 1 - .../order/[orderId]/confirmed/page.tsx | 43 +++++++++++---- .../(dashboard)/order/[orderId]/page.tsx | 52 +++++++++++++++++++ app/home/(user)/(dashboard)/order/page.tsx | 33 ++++++++++-- .../(user)/_components/order/cart-totals.tsx | 6 +-- .../_components/order/order-completed.tsx | 27 ---------- .../_components/order/order-details.tsx | 38 +++----------- .../(user)/_components/order/order-item.tsx | 7 +-- .../(user)/_components/order/order-items.tsx | 8 +-- .../(user)/_components/orders/orders-item.tsx | 11 ++-- .../_components/orders/orders-table.tsx | 9 ++-- app/home/(user)/_components/orders/types.ts | 3 +- lib/services/order.service.ts | 16 +++++- public/locales/en/cart.json | 3 ++ public/locales/en/orders.json | 11 +++- public/locales/et/cart.json | 3 ++ public/locales/et/orders.json | 11 +++- 19 files changed, 185 insertions(+), 105 deletions(-) create mode 100644 app/home/(user)/(dashboard)/order/[orderId]/page.tsx delete mode 100644 app/home/(user)/_components/order/order-completed.tsx diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index e48ac85..63a9bb9 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -91,7 +91,7 @@ export async function processMontonioCallback(orderToken: string) { const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false }); const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder }); - await createOrder({ medusaOrder, orderedAnalysisElements }); + const orderId = await createOrder({ medusaOrder, orderedAnalysisElements }); const { productTypes } = await listProductTypes(); const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE); @@ -122,7 +122,7 @@ export async function processMontonioCallback(orderToken: string) { // Send order to Medipost (no await to avoid blocking) sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); - return { success: true }; + return { success: true, orderId }; } catch (error) { console.error("Failed to place order", error); throw new Error(`Failed to place order, message=${error}`); diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/client-component.tsx b/app/home/(user)/(dashboard)/cart/montonio-callback/client-component.tsx index f90efa4..c388a6d 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/client-component.tsx +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/client-component.tsx @@ -29,8 +29,8 @@ export default function MontonioCallbackClient({ orderToken, error }: { setHasProcessed(true); try { - await processMontonioCallback(orderToken); - router.push('/home/order'); + const { orderId } = await processMontonioCallback(orderToken); + router.push(`/home/order/${orderId}/confirmed`); } catch (error) { console.error("Failed to place order", error); router.push('/home/cart/montonio-callback/error'); diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/page.tsx b/app/home/(user)/(dashboard)/cart/montonio-callback/page.tsx index 6893a97..d4f54f4 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/page.tsx +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/page.tsx @@ -7,7 +7,6 @@ export default async function MontonioCallbackPage({ searchParams }: { }) { const orderToken = (await searchParams)['order-token']; - console.log('orderToken', orderToken); if (!orderToken) { return ; } diff --git a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx index 2ca1b55..f49e5cb 100644 --- a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx +++ b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx @@ -1,13 +1,16 @@ -import { notFound } from 'next/navigation'; +import { redirect } from 'next/navigation'; +import { PageBody, PageHeader } from '@kit/ui/page'; -import { retrieveOrder } from '~/medusa/lib/data/orders'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; -import OrderCompleted from '@/app/home/(user)/_components/order/order-completed'; import { withI18n } from '~/lib/i18n/with-i18n'; - -type Props = { - params: Promise<{ orderId: string }>; -}; +import { getOrder } from '~/lib/services/order.service'; +import { retrieveOrder } from '@lib/data/orders'; +import pathsConfig from '~/config/paths.config'; +import Divider from "@modules/common/components/divider" +import OrderDetails from '@/app/home/(user)/_components/order/order-details'; +import OrderItems from '@/app/home/(user)/_components/order/order-items'; +import CartTotals from '@/app/home/(user)/_components/order/cart-totals'; +import { Trans } from '@kit/ui/trans'; export async function generateMetadata() { const { t } = await createI18nServerInstance(); @@ -17,15 +20,33 @@ export async function generateMetadata() { }; } -async function OrderConfirmedPage(props: Props) { +async function OrderConfirmedPage(props: { + params: Promise<{ orderId: string }>; +}) { const params = await props.params; - const order = await retrieveOrder(params.orderId).catch(() => null); + const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null); if (!order) { - return notFound(); + redirect(pathsConfig.app.myOrders); } - return ; + const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null); + if (!medusaOrder) { + redirect(pathsConfig.app.myOrders); + } + + return ( + + } /> + +
+ + + + +
+
+ ); } export default withI18n(OrderConfirmedPage); diff --git a/app/home/(user)/(dashboard)/order/[orderId]/page.tsx b/app/home/(user)/(dashboard)/order/[orderId]/page.tsx new file mode 100644 index 0000000..b9e8597 --- /dev/null +++ b/app/home/(user)/(dashboard)/order/[orderId]/page.tsx @@ -0,0 +1,52 @@ +import { redirect } from 'next/navigation'; +import { PageBody, PageHeader } from '@kit/ui/page'; + +import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; +import { getOrder } from '~/lib/services/order.service'; +import { retrieveOrder } from '@lib/data/orders'; +import pathsConfig from '~/config/paths.config'; +import Divider from "@modules/common/components/divider" +import OrderDetails from '@/app/home/(user)/_components/order/order-details'; +import OrderItems from '@/app/home/(user)/_components/order/order-items'; +import CartTotals from '@/app/home/(user)/_components/order/cart-totals'; +import { Trans } from '@kit/ui/trans'; + +export async function generateMetadata() { + const { t } = await createI18nServerInstance(); + + return { + title: t('cart:order.title'), + }; +} + +async function OrderConfirmedPage(props: { + params: Promise<{ orderId: string }>; +}) { + const params = await props.params; + + const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null); + if (!order) { + redirect(pathsConfig.app.myOrders); + } + + const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null); + if (!medusaOrder) { + redirect(pathsConfig.app.myOrders); + } + + return ( + + } /> + +
+ + + + +
+
+ ); +} + +export default withI18n(OrderConfirmedPage); diff --git a/app/home/(user)/(dashboard)/order/page.tsx b/app/home/(user)/(dashboard)/order/page.tsx index 97144af..7b038fe 100644 --- a/app/home/(user)/(dashboard)/order/page.tsx +++ b/app/home/(user)/(dashboard)/order/page.tsx @@ -10,6 +10,7 @@ import { HomeLayoutPageHeader } from '../../_components/home-page-header'; import OrdersTable from '../../_components/orders/orders-table'; import { withI18n } from '~/lib/i18n/with-i18n'; import type { IOrderLineItem } from '../../_components/orders/types'; +import { getOrders } from '~/lib/services/order.service'; export async function generateMetadata() { const { t } = await createI18nServerInstance(); @@ -20,7 +21,8 @@ export async function generateMetadata() { } async function OrdersPage() { - const orders = await listOrders().catch(() => null); + const orders = await listOrders(); + const localOrders = await getOrders(); const { productTypes } = await listProductTypes(); if (!orders || !productTypes) { @@ -30,13 +32,38 @@ async function OrdersPage() { const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages'); const analysisPackageOrders: IOrderLineItem[] = orders.flatMap(({ id, items, payment_status, fulfillment_status }) => items ?.filter((item) => item.product_type_id === analysisPackagesType?.id) - .map((item) => ({ item, orderId: id, orderStatus: `${payment_status}/${fulfillment_status}` })) + .map((item) => { + const localOrder = localOrders.find((order) => order.medusa_order_id === id); + if (!localOrder) { + return null; + } + return { + item, + medusaOrderId: id, + orderId: localOrder?.id, + orderStatus: localOrder.status, + analysis_element_ids: localOrder.analysis_element_ids, + } + }) + .filter((order) => order !== null) || []); const otherOrders: IOrderLineItem[] = orders .filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id)) .flatMap(({ id, items, payment_status, fulfillment_status }) => items - ?.map((item) => ({ item, orderId: id, orderStatus: `${payment_status}/${fulfillment_status}` })) + ?.map((item) => { + const localOrder = localOrders.find((order) => order.medusa_order_id === id); + if (!localOrder) { + return null; + } + return { + item, + medusaOrderId: id, + orderId: localOrder.id, + orderStatus: localOrder.status, + } + }) + .filter((order) => order !== null) || []); return ( diff --git a/app/home/(user)/_components/order/cart-totals.tsx b/app/home/(user)/_components/order/cart-totals.tsx index dc25aad..2df2237 100644 --- a/app/home/(user)/_components/order/cart-totals.tsx +++ b/app/home/(user)/_components/order/cart-totals.tsx @@ -6,8 +6,8 @@ import React from "react" import { useTranslation } from "react-i18next" import { Trans } from '@kit/ui/trans'; -export default function CartTotals({ order }: { - order: StoreOrder +export default function CartTotals({ medusaOrder }: { + medusaOrder: StoreOrder }) { const { i18n: { language } } = useTranslation() const { @@ -17,7 +17,7 @@ export default function CartTotals({ order }: { tax_total, discount_total, gift_card_total, - } = order + } = medusaOrder return (
diff --git a/app/home/(user)/_components/order/order-completed.tsx b/app/home/(user)/_components/order/order-completed.tsx deleted file mode 100644 index f7529a0..0000000 --- a/app/home/(user)/_components/order/order-completed.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Trans } from '@kit/ui/trans'; -import { PageBody, PageHeader } from '@kit/ui/page'; -import { StoreOrder } from "@medusajs/types" -import Divider from "@modules/common/components/divider" - -import CartTotals from "./cart-totals" -import OrderDetails from "./order-details" -import OrderItems from "./order-items" - -export default async function OrderCompleted({ - order, -}: { - order: StoreOrder, -}) { - return ( - - } /> - -
- - - - -
-
- ) -} diff --git a/app/home/(user)/_components/order/order-details.tsx b/app/home/(user)/_components/order/order-details.tsx index e6bc6cf..b750c85 100644 --- a/app/home/(user)/_components/order/order-details.tsx +++ b/app/home/(user)/_components/order/order-details.tsx @@ -1,47 +1,21 @@ -import { StoreOrder } from "@medusajs/types" import { Trans } from '@kit/ui/trans'; +import { formatDate } from 'date-fns'; +import { AnalysisOrder } from "~/lib/services/order.service"; -export default function OrderDetails({ order, showStatus }: { - order: StoreOrder - showStatus?: boolean +export default function OrderDetails({ order }: { + order: AnalysisOrder }) { - const formatStatus = (str: string) => { - const formatted = str.split("_").join(" ") - - return formatted.slice(0, 1).toUpperCase() + formatted.slice(1) - } - return (
:{" "} - {new Date(order.created_at).toLocaleDateString()} + {formatDate(order.created_at, 'dd.MM.yyyy HH:mm')} - : {order.display_id} + : {order.medusa_order_id} - - {showStatus && ( - <> - - :{" "} - - {formatStatus(order.fulfillment_status)} - - - - :{" "} - - {formatStatus(order.payment_status)} - - - - )}
) } diff --git a/app/home/(user)/_components/order/order-item.tsx b/app/home/(user)/_components/order/order-item.tsx index 4cd4e7a..cad98e6 100644 --- a/app/home/(user)/_components/order/order-item.tsx +++ b/app/home/(user)/_components/order/order-item.tsx @@ -1,7 +1,7 @@ import { StoreCartLineItem, StoreOrderLineItem } from "@medusajs/types" import { TableCell, TableRow } from "@kit/ui/table" -import LineItemOptions from "@modules/common/components/line-item-options" +// import LineItemOptions from "@modules/common/components/line-item-options" import LineItemPrice from "@modules/common/components/line-item-price" import LineItemUnitPrice from "@modules/common/components/line-item-unit-price" @@ -9,6 +9,7 @@ export default function OrderItem({ item, currencyCode }: { item: StoreCartLineItem | StoreOrderLineItem currencyCode: string }) { + const partnerLocationName = item.metadata?.partner_location_name; return ( {/* @@ -22,9 +23,9 @@ export default function OrderItem({ item, currencyCode }: { className="txt-medium-plus text-ui-fg-base" data-testid="product-name" > - {item.product_title}{` (${item.metadata?.partner_location_name ?? "-"})`} + {item.product_title}{` ${partnerLocationName ? `(${partnerLocationName})` : ''}`} - + {/* */} diff --git a/app/home/(user)/_components/order/order-items.tsx b/app/home/(user)/_components/order/order-items.tsx index 25dbe31..5375314 100644 --- a/app/home/(user)/_components/order/order-items.tsx +++ b/app/home/(user)/_components/order/order-items.tsx @@ -7,10 +7,10 @@ import OrderItem from "./order-item" import { Heading } from "@kit/ui/heading" import { Trans } from '@kit/ui/trans'; -export default function OrderItems({ order }: { - order: StoreOrder +export default function OrderItems({ medusaOrder }: { + medusaOrder: StoreOrder }) { - const items = order.items + const items = medusaOrder.items return (
@@ -27,7 +27,7 @@ export default function OrderItems({ order }: { )) : repeat(5).map((i) => )} diff --git a/app/home/(user)/_components/orders/orders-item.tsx b/app/home/(user)/_components/orders/orders-item.tsx index 2d80f7d..ea8943d 100644 --- a/app/home/(user)/_components/orders/orders-item.tsx +++ b/app/home/(user)/_components/orders/orders-item.tsx @@ -6,6 +6,7 @@ import { Eye } from "lucide-react"; import Link from "next/link"; import { formatDate } from "date-fns"; import { IOrderLineItem } from "./types"; +import { Trans } from '@kit/ui/trans'; export default function OrdersItem({ orderItem }: { orderItem: IOrderLineItem, @@ -22,15 +23,13 @@ export default function OrdersItem({ orderItem }: { {formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')} - {orderItem.orderStatus && ( - - {orderItem.orderStatus} - - )} + + + - +