feat(MED-131): show package analysis elements in comparison modal

This commit is contained in:
2025-08-04 11:51:38 +03:00
parent d7d089c11d
commit 8c4df731aa
7 changed files with 224 additions and 106 deletions

View File

@@ -20,7 +20,7 @@ export const generateMetadata = async () => {
}; };
async function OrderAnalysisPackagePage() { async function OrderAnalysisPackagePage() {
const { analysisPackages, countryCode } = await loadAnalysisPackages(); const { analysisElements, analysisPackages, countryCode } = await loadAnalysisPackages();
return ( return (
<PageBody> <PageBody>
@@ -29,6 +29,7 @@ async function OrderAnalysisPackagePage() {
<Trans i18nKey={'marketing:selectPackage'} /> <Trans i18nKey={'marketing:selectPackage'} />
</h3> </h3>
<ComparePackagesModal <ComparePackagesModal
analysisElements={analysisElements}
analysisPackages={analysisPackages} analysisPackages={analysisPackages}
triggerElement={ triggerElement={
<Button variant="secondary" className="gap-2"> <Button variant="secondary" className="gap-2">

View File

@@ -23,79 +23,8 @@ import { withI18n } from '~/lib/i18n/with-i18n';
import { PackageHeader } from '@/components/package-header'; import { PackageHeader } from '@/components/package-header';
import { InfoTooltip } from '@/components/ui/info-tooltip'; import { InfoTooltip } from '@/components/ui/info-tooltip';
import { StoreProduct } from '@medusajs/types'; import { StoreProduct } from '@medusajs/types';
import type { AnalysisElement } from '~/lib/services/analysis-element.service';
const dummyCards = [ import { getAnalysisElementOriginalIds } from '@lib/data/products';
{
titleKey: 'product:standard.label',
price: 40,
nrOfAnalyses: 4,
tagColor: 'bg-cyan',
},
{
titleKey: 'product:standardPlus.label',
price: 85,
nrOfAnalyses: 10,
tagColor: 'bg-warning',
},
{
titleKey: 'product:premium.label',
price: 140,
nrOfAnalyses: '12+',
tagColor: 'bg-purple',
},
];
const dummyRows = [
{
analysisNameKey: 'product:clinicalBloodDraw.label',
tooltipContentKey: 'product:clinicalBloodDraw.description',
includedInStandard: 1,
includedInStandardPlus: 1,
includedInPremium: 1,
},
{
analysisNameKey: 'product:crp.label',
tooltipContentKey: 'product:crp.description',
includedInStandard: 1,
includedInStandardPlus: 1,
includedInPremium: 1,
},
{
analysisNameKey: 'product:ferritin.label',
tooltipContentKey: 'product:ferritin.description',
includedInStandard: 0,
includedInStandardPlus: 1,
includedInPremium: 1,
},
{
analysisNameKey: 'product:vitaminD.label',
tooltipContentKey: 'product:vitaminD.description',
includedInStandard: 0,
includedInStandardPlus: 1,
includedInPremium: 1,
},
{
analysisNameKey: 'product:glucose.label',
tooltipContentKey: 'product:glucose.description',
includedInStandard: 1,
includedInStandardPlus: 1,
includedInPremium: 1,
},
{
analysisNameKey: 'product:alat.label',
tooltipContentKey: 'product:alat.description',
includedInStandard: 1,
includedInStandardPlus: 1,
includedInPremium: 1,
},
{
analysisNameKey: 'product:ast.label',
tooltipContentKey: 'product:ast.description',
includedInStandard: 1,
includedInStandardPlus: 1,
includedInPremium: 1,
},
];
const CheckWithBackground = () => { const CheckWithBackground = () => {
return ( return (
@@ -106,14 +35,28 @@ const CheckWithBackground = () => {
}; };
const ComparePackagesModal = async ({ const ComparePackagesModal = async ({
analysisElements,
analysisPackages, analysisPackages,
triggerElement, triggerElement,
}: { }: {
analysisElements: AnalysisElement[];
analysisPackages: StoreProduct[]; analysisPackages: StoreProduct[];
triggerElement: JSX.Element; triggerElement: JSX.Element;
}) => { }) => {
const { t, language } = await createI18nServerInstance(); const { t, language } = await createI18nServerInstance();
const standardPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'standard')!;
const standardPlusPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'standard-plus')!;
const premiumPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'premium')!;
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
return null;
}
const standardPackageAnalyses = await getAnalysisElementOriginalIds([standardPackage]);
const standardPlusPackageAnalyses = await getAnalysisElementOriginalIds([standardPlusPackage]);
const premiumPackageAnalyses = await getAnalysisElementOriginalIds([premiumPackage]);
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild>{triggerElement}</DialogTrigger> <DialogTrigger asChild>{triggerElement}</DialogTrigger>
@@ -138,7 +81,7 @@ const ComparePackagesModal = async ({
<p className="text-muted-foreground mx-auto w-3/5 text-sm"> <p className="text-muted-foreground mx-auto w-3/5 text-sm">
{t('product:healthPackageComparison.description')} {t('product:healthPackageComparison.description')}
</p> </p>
<div className="rounded-md border"> <div className="rounded-md border max-h-[80vh] overflow-y-auto">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@@ -165,37 +108,41 @@ const ComparePackagesModal = async ({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{dummyRows.map( {analysisElements.map(
( (
{ {
analysisNameKey, analysis_name_lab: analysisName,
tooltipContentKey, analysis_id_original: analysisId,
includedInStandard,
includedInStandardPlus,
includedInPremium,
}, },
index, index,
) => ( ) => {
<TableRow key={index}> if (!analysisName) {
<TableCell className="py-6"> return null;
{t(analysisNameKey)}{' '} }
<InfoTooltip const includedInStandard = standardPackageAnalyses.includes(analysisId);
content={t(tooltipContentKey)} const includedInStandardPlus = standardPlusPackageAnalyses.includes(analysisId);
icon={<QuestionMarkCircledIcon />} const includedInPremium = premiumPackageAnalyses.includes(analysisId);
/> return (
</TableCell> <TableRow key={index}>
<TableCell align="center" className="py-6"> <TableCell className="py-6">
{!!includedInStandard && <CheckWithBackground />} {analysisName}{' '}
</TableCell> {/* <InfoTooltip
<TableCell align="center" className="py-6"> content={t(tooltipContentKey)}
{!!includedInStandardPlus && <CheckWithBackground />} icon={<QuestionMarkCircledIcon />}
</TableCell> /> */}
<TableCell align="center" className="py-6"> </TableCell>
{!!includedInPremium && <CheckWithBackground />} <TableCell align="center" className="py-6">
</TableCell> {includedInStandard && <CheckWithBackground />}
</TableRow> </TableCell>
), <TableCell align="center" className="py-6">
)} {(includedInStandard || includedInStandardPlus) && <CheckWithBackground />}
</TableCell>
<TableCell align="center" className="py-6">
{(includedInStandard || includedInStandardPlus || includedInPremium) && <CheckWithBackground />}
</TableCell>
</TableRow>
);
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>

View File

@@ -1,6 +1,8 @@
import { cache } from 'react'; import { cache } from 'react';
import { listProductTypes, listProducts, listRegions } from "@lib/data"; import { getAnalysisElementOriginalIds, listProductTypes, listProducts } from "@lib/data/products";
import { listRegions } from '@lib/data/regions';
import { AnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
async function countryCodesLoader() { async function countryCodesLoader() {
const countryCodes = await listRegions().then((regions) => const countryCodes = await listRegions().then((regions) =>
@@ -22,13 +24,21 @@ async function analysisPackagesLoader() {
const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages'); const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
if (!productType) { if (!productType) {
return { analysisPackages: [], countryCode }; return { analysisElements: [], analysisPackages: [], countryCode };
} }
const { response } = await listProducts({ const { response } = await listProducts({
countryCode, countryCode,
queryParams: { limit: 100, "type_id[0]": productType.id }, queryParams: { limit: 100, "type_id[0]": productType.id },
}); });
return { analysisPackages: response.products, countryCode }; const analysisPackages = response.products;
let analysisElements: AnalysisElement[] = [];
const analysisElementOriginalIds = await getAnalysisElementOriginalIds(analysisPackages);
if (analysisElementOriginalIds.length) {
analysisElements = await getAnalysisElements({ originalIds: analysisElementOriginalIds });
}
return { analysisElements, analysisPackages, countryCode };
} }
export const loadAnalysisPackages = cache(analysisPackagesLoader); export const loadAnalysisPackages = cache(analysisPackagesLoader);

View File

@@ -24,7 +24,7 @@ export const generateMetadata = async () => {
}; };
async function SelectPackagePage() { async function SelectPackagePage() {
const { analysisPackages, countryCode } = await loadAnalysisPackages(); const { analysisElements, analysisPackages, countryCode } = await loadAnalysisPackages();
return ( return (
<div className="container mx-auto my-24 flex flex-col items-center space-y-12"> <div className="container mx-auto my-24 flex flex-col items-center space-y-12">
@@ -34,6 +34,7 @@ async function SelectPackagePage() {
<Trans i18nKey={'marketing:selectPackage'} /> <Trans i18nKey={'marketing:selectPackage'} />
</h3> </h3>
<ComparePackagesModal <ComparePackagesModal
analysisElements={analysisElements}
analysisPackages={analysisPackages} analysisPackages={analysisPackages}
triggerElement={ triggerElement={
<Button variant="secondary" className="gap-2"> <Button variant="secondary" className="gap-2">

View File

@@ -0,0 +1,60 @@
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';
export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
};
export async function getAnalysisElements({
originalIds,
}: {
originalIds: string[]
}) {
const { data: analysisElements } = await getSupabaseServerClient()
.schema('medreport')
.from('analysis_elements')
.select(`*, analysis_groups(*)`)
.in('analysis_id_original', [...new Set(originalIds)])
.order('order', { ascending: true });
return analysisElements ?? [];
}
export async function createAnalysisElement({
analysisElement,
analysisGroupId,
materialGroups,
}: {
analysisElement: IUuringElement;
analysisGroupId: number;
materialGroups: IMaterialGroup[];
}) {
const { data: insertedAnalysisElement, error } = await getSupabaseServerAdminClient()
.schema('medreport')
.from('analysis_elements')
.upsert(
{
analysis_id_oid: analysisElement.UuringIdOID,
analysis_id_original: analysisElement.UuringId,
tehik_short_loinc: analysisElement.TLyhend,
tehik_loinc_name: analysisElement.KNimetus,
analysis_name_lab: analysisElement.UuringNimi,
order: analysisElement.Jarjekord,
parent_analysis_group_id: analysisGroupId,
material_groups: materialGroups as unknown as Json[],
},
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
)
.select('id');
const id = insertedAnalysisElement?.[0]?.id;
if (error || !id) {
throw new Error(
`Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
);
}
return id;
}

View File

@@ -0,0 +1,85 @@
export interface IUuringElement {
UuringIdOID: string;
UuringId: string;
TLyhend: string;
KNimetus: string;
UuringNimi: string;
Jarjekord: number;
Kood: {
HkKood: string;
HkKoodiKordaja: number;
Koefitsient: number;
Hind: number;
}[];
UuringuElement: {
UuringIdOID: string;
UuringId: string;
TLyhend: string;
KNimetus: string;
UuringNimi: string;
Jarjekord: number;
Kood: {
HkKood: string;
HkKoodiKordaja: number;
Koefitsient: number;
Hind: number;
}[];
}[];
}
export interface IMaterialGroup {
id: string;
name: string;
order: number;
}
export interface IMedipostPublicMessageDataParsed {
ANSWER: {
CODE: number;
MESSAGE: string;
};
Saadetis: {
Teenused: {
Teostaja: {
UuringuGrupp: {
UuringuGruppId: string;
UuringuGruppNimi: string;
UuringuGruppJarjekord: number;
Kood: {
HkKood: string;
HkKoodiKordaja: number;
Koefitsient: number;
Hind: number;
}[];
Uuring: {
UuringId: string;
UuringNimi: string;
UuringJarjekord: number;
UuringuElement: {
UuringIdOID: string;
UuringId: string;
TLyhend: string;
KNimetus: string;
UuringNimi: string;
Jarjekord: number;
Kood: {
HkKood: string;
HkKoodiKordaja: number;
Koefitsient: number;
Hind: number;
}[];
UuringuElement: IUuringElement;
}[];
MaterjalideGrupp: IMaterialGroup[];
Kood: {
HkKood: string;
HkKoodiKordaja: number;
Koefitsient: number;
Hind: number;
}[];
}[];
}[];
}[];
};
};
}

View File

@@ -85,6 +85,20 @@ export const listProducts = async ({
}) })
} }
export const getAnalysisElementOriginalIds = async (products: HttpTypes.StoreProduct[]) => {
return products
.flatMap(({ metadata }) => {
const value = metadata?.analysisElementOriginalIds;
try {
return JSON.parse(value as string);
} catch (e) {
console.error("Failed to parse analysisElementOriginalIds from analysis package, possibly invalid format", e);
return [];
}
})
.filter(Boolean) as string[];
}
/** /**
* This will fetch 100 products to the Next.js cache and sort them based on the sortBy parameter. * This will fetch 100 products to the Next.js cache and sort them based on the sortBy parameter.
* It will then return the paginated products based on the page and limit parameters. * It will then return the paginated products based on the page and limit parameters.