Merge branch 'develop' into MED-157
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
@@ -32,7 +33,7 @@ export default async function AnalysisResultsPage({
|
||||
]);
|
||||
|
||||
if (!account?.id) {
|
||||
return redirect("/");
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
await createPageViewLog({
|
||||
@@ -47,27 +48,31 @@ export default async function AnalysisResultsPage({
|
||||
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||
description={<Trans i18nKey="analysis-results:descriptionEmpty" />}
|
||||
/>
|
||||
<PageBody className="gap-4">
|
||||
</PageBody>
|
||||
<PageBody className="gap-4"></PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
|
||||
const hasOrderedAnalysisElements = orderedAnalysisElements.length > 0;
|
||||
const isPartialStatus = analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
|
||||
const isPartialStatus =
|
||||
analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||
description={hasOrderedAnalysisElements ? (
|
||||
isPartialStatus
|
||||
? <Trans i18nKey="analysis-results:descriptionPartial" />
|
||||
: <Trans i18nKey="analysis-results:description" />
|
||||
) : (
|
||||
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
||||
)}
|
||||
description={
|
||||
hasOrderedAnalysisElements ? (
|
||||
isPartialStatus ? (
|
||||
<Trans i18nKey="analysis-results:descriptionPartial" />
|
||||
) : (
|
||||
<Trans i18nKey="analysis-results:description" />
|
||||
)
|
||||
) : (
|
||||
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Button asChild>
|
||||
@@ -106,13 +111,15 @@ export default async function AnalysisResultsPage({
|
||||
orderedAnalysisElements.map((element, index) => (
|
||||
<React.Fragment key={element.analysisIdOriginal}>
|
||||
<Analysis element={element} />
|
||||
{element.results?.nestedElements?.map((nestedElement, nestedIndex) => (
|
||||
<Analysis
|
||||
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
||||
nestedElement={nestedElement}
|
||||
isNestedElement
|
||||
/>
|
||||
))}
|
||||
{element.results?.nestedElements?.map(
|
||||
(nestedElement, nestedIndex) => (
|
||||
<Analysis
|
||||
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
||||
nestedElement={nestedElement}
|
||||
isNestedElement
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { AnalysisResultDetailsElementResults } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { ArrowDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
import type { AnalysisResultDetailsElementResults } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
|
||||
type AnalysisResultLevelBarResults = Pick<AnalysisResultDetailsElementResults, 'normLower' | 'normUpper' | 'responseValue'>;
|
||||
type AnalysisResultLevelBarResults = Pick<
|
||||
AnalysisResultDetailsElementResults,
|
||||
'normLower' | 'normUpper' | 'responseValue'
|
||||
>;
|
||||
|
||||
const Level = ({
|
||||
isActive = false,
|
||||
@@ -34,22 +37,29 @@ const Level = ({
|
||||
{isActive && (
|
||||
<div
|
||||
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
||||
{...(arrowLocation ? {
|
||||
style: {
|
||||
left: `${arrowLocation}%`,
|
||||
...(arrowLocation > 92.5 && { left: '92.5%' }),
|
||||
...(arrowLocation < 7.5 && { left: '7.5%' }),
|
||||
}
|
||||
} : {})}
|
||||
{...(arrowLocation
|
||||
? {
|
||||
style: {
|
||||
left: `${arrowLocation}%`,
|
||||
...(arrowLocation > 92.5 && { left: '92.5%' }),
|
||||
...(arrowLocation < 7.5 && { left: '7.5%' }),
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<ArrowDown strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{color === 'success' && typeof normRangeText === 'string' && (
|
||||
<p className={cn("absolute bottom-[-18px] left-3/8 text-xs text-muted-foreground font-bold whitespace-nowrap", {
|
||||
'opacity-60': isActive,
|
||||
})}>
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground absolute bottom-[-18px] left-3/8 text-xs font-bold whitespace-nowrap',
|
||||
{
|
||||
'opacity-60': isActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{normRangeText}
|
||||
</p>
|
||||
)}
|
||||
@@ -59,11 +69,7 @@ const Level = ({
|
||||
|
||||
const AnalysisLevelBar = ({
|
||||
level,
|
||||
results: {
|
||||
normLower: lower,
|
||||
normUpper: upper,
|
||||
responseValue: value,
|
||||
},
|
||||
results: { normLower: lower, normUpper: upper, responseValue: value },
|
||||
normRangeText,
|
||||
}: {
|
||||
level: AnalysisResultLevel;
|
||||
@@ -90,7 +96,7 @@ const AnalysisLevelBar = ({
|
||||
return 100; // Beyond upper bound
|
||||
}
|
||||
|
||||
// If only lower bound exists
|
||||
// If only lower bound exists
|
||||
if (upper === null && lower !== null) {
|
||||
if (value >= lower) {
|
||||
// Value is in normal range (above lower bound)
|
||||
@@ -127,7 +133,7 @@ const AnalysisLevelBar = ({
|
||||
// If pending results, show gray bar
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="mt-4 flex h-3 w-60% sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
||||
<div className="w-60% mt-4 flex h-3 max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||
<Level color="gray-200" isFirst isLast />
|
||||
</div>
|
||||
);
|
||||
@@ -146,29 +152,25 @@ const AnalysisLevelBar = ({
|
||||
const [warning, normal, critical] = [
|
||||
{
|
||||
isActive: isWarning,
|
||||
color: "warning",
|
||||
color: 'warning',
|
||||
...(isWarning ? { arrowLocation } : {}),
|
||||
},
|
||||
{
|
||||
isActive: isNormal,
|
||||
color: "success",
|
||||
color: 'success',
|
||||
normRangeText,
|
||||
...(isNormal ? { arrowLocation } : {}),
|
||||
},
|
||||
{
|
||||
isActive: isCritical,
|
||||
color: "destructive",
|
||||
color: 'destructive',
|
||||
isLast: true,
|
||||
...(isCritical ? { arrowLocation } : {}),
|
||||
},
|
||||
] as const;
|
||||
|
||||
if (!hasLowerBound) {
|
||||
return [
|
||||
{ ...normal, isFirst: true },
|
||||
warning,
|
||||
critical,
|
||||
] as const;
|
||||
return [{ ...normal, isFirst: true }, warning, critical] as const;
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -176,16 +178,27 @@ const AnalysisLevelBar = ({
|
||||
normal,
|
||||
{ ...critical, isLast: true },
|
||||
] as const;
|
||||
}, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]);
|
||||
}, [
|
||||
isValueBelowLower,
|
||||
isValueAboveUpper,
|
||||
isValueInNormalRange,
|
||||
arrowLocation,
|
||||
normRangeText,
|
||||
isNormal,
|
||||
isWarning,
|
||||
isCritical,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex h-3 gap-1",
|
||||
"mt-4 sm:mt-0",
|
||||
"w-[60%] sm:w-[35%]",
|
||||
"min-w-[50vw] sm:min-w-auto",
|
||||
"max-w-[360px]",
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-3 gap-1',
|
||||
'mt-4 sm:mt-0',
|
||||
'w-[60%] sm:w-[35%]',
|
||||
'min-w-[50vw] sm:min-w-auto',
|
||||
'max-w-[360px]',
|
||||
)}
|
||||
>
|
||||
<Level {...first} />
|
||||
<Level {...second} />
|
||||
<Level {...third} />
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { format } from 'date-fns';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import type {
|
||||
AnalysisResultDetailsElement,
|
||||
AnalysisResultsDetailsElementNested,
|
||||
} from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { format } from 'date-fns';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
@@ -64,7 +65,11 @@ const Analysis = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { responseValue, responseValueIsNegative, responseValueIsWithinNorm } = results;
|
||||
const {
|
||||
responseValue,
|
||||
responseValueIsNegative,
|
||||
responseValueIsWithinNorm,
|
||||
} = results;
|
||||
if (responseValue === null || responseValue === undefined) {
|
||||
if (hasIsNegative) {
|
||||
if (responseValueIsNegative) {
|
||||
@@ -107,7 +112,8 @@ const Analysis = ({
|
||||
|
||||
const isCancelled = Number(results?.status) === 5;
|
||||
const nestedElements = results?.nestedElements ?? null;
|
||||
const hasNestedElements = Array.isArray(nestedElements) && nestedElements.length > 0;
|
||||
const hasNestedElements =
|
||||
Array.isArray(nestedElements) && nestedElements.length > 0;
|
||||
|
||||
const normRangeText = (() => {
|
||||
if (normLower === null && normUpper === null) {
|
||||
@@ -118,9 +124,17 @@ const Analysis = ({
|
||||
const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
|
||||
|
||||
return (
|
||||
<div className={cn("border-border rounded-lg border px-5", { 'ml-8': isNestedElement })}>
|
||||
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
||||
<div className={cn("flex items-center gap-2 font-semibold", { 'font-bold': isNestedElement })}>
|
||||
<div
|
||||
className={cn('border-border rounded-lg border px-5', {
|
||||
'ml-8': isNestedElement,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:h-[65px] sm:flex-row sm:gap-0 sm:py-3">
|
||||
<div
|
||||
className={cn('flex items-center gap-2 font-semibold', {
|
||||
'font-bold': isNestedElement,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
{results?.responseTime && (
|
||||
<div
|
||||
@@ -147,7 +161,7 @@ const Analysis = ({
|
||||
</div>
|
||||
|
||||
{isCancelled && (
|
||||
<div className="text-red-600 font-semibold text-sm">
|
||||
<div className="text-sm font-semibold text-red-600">
|
||||
<Trans i18nKey="analysis-results:cancelled" />
|
||||
</div>
|
||||
)}
|
||||
@@ -157,9 +171,15 @@ const Analysis = ({
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<div
|
||||
className={cn('font-semibold', {
|
||||
'text-yellow-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.WARNING,
|
||||
'text-red-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.CRITICAL,
|
||||
'text-green-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.NORMAL,
|
||||
'text-yellow-600':
|
||||
hasTextualResponse &&
|
||||
analysisResultLevel === AnalysisResultLevel.WARNING,
|
||||
'text-red-600':
|
||||
hasTextualResponse &&
|
||||
analysisResultLevel === AnalysisResultLevel.CRITICAL,
|
||||
'text-green-600':
|
||||
hasTextualResponse &&
|
||||
analysisResultLevel === AnalysisResultLevel.NORMAL,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import Modal from '@modules/common/components/modal';
|
||||
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import Modal from "@modules/common/components/modal"
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import Analysis from '../_components/analysis';
|
||||
import { analysisResponses } from './test-responses';
|
||||
@@ -19,16 +20,15 @@ export default function AnalysisResultsPage() {
|
||||
<PageBody className="gap-4">
|
||||
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
|
||||
<div>
|
||||
<h2>
|
||||
Analüüsi tulemused demo
|
||||
</h2>
|
||||
<h2>Analüüsi tulemused demo</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{analysisResponses.map(({ id, orderedAnalysisElements }, index) => {
|
||||
const isOpen = openBlocks.includes(id);
|
||||
const closeModal = () => setOpenBlocks(openBlocks.filter((block) => block !== id));
|
||||
const closeModal = () =>
|
||||
setOpenBlocks(openBlocks.filter((block) => block !== id));
|
||||
return (
|
||||
<div key={index} className="flex flex-col gap-2 py-4">
|
||||
<div className="flex flex-col gap-2 pb-4">
|
||||
@@ -52,12 +52,21 @@ export default function AnalysisResultsPage() {
|
||||
{isOpen && (
|
||||
<Modal isOpen={isOpen} close={closeModal} size="large">
|
||||
<div className="overflow-y-auto">
|
||||
|
||||
<p>NormiStaatus</p>
|
||||
<ul>
|
||||
<li>0 - testi väärtus jääb normaalväärtuste piirkonda või on määramata,</li>
|
||||
<li>1 - testi väärtus jääb hoiatava (tähelepanu suunava) märkega piirkonda,</li>
|
||||
<li>2 - testi väärtus on normaalväärtuste piirkonnast väljas või kõrgendatud tähelepanu nõudvas piirkonnas.</li>
|
||||
<li>
|
||||
0 - testi väärtus jääb normaalväärtuste piirkonda
|
||||
või on määramata,
|
||||
</li>
|
||||
<li>
|
||||
1 - testi väärtus jääb hoiatava (tähelepanu
|
||||
suunava) märkega piirkonda,
|
||||
</li>
|
||||
<li>
|
||||
2 - testi väärtus on normaalväärtuste piirkonnast
|
||||
väljas või kõrgendatud tähelepanu nõudvas
|
||||
piirkonnas.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>UuringOlek</p>
|
||||
@@ -70,7 +79,7 @@ export default function AnalysisResultsPage() {
|
||||
<li>6 - Tühistatud,</li>
|
||||
</ul>
|
||||
|
||||
<pre className="text-sm bg-muted p-4 rounded-md">
|
||||
<pre className="bg-muted rounded-md p-4 text-sm">
|
||||
{JSON.stringify(orderedAnalysisElements, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -90,7 +99,7 @@ export default function AnalysisResultsPage() {
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PageBody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnalysisResultDetailsMapped } from '@/packages/features/accounts/src/types/analysis-results';
|
||||
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
|
||||
export type AnalysisTestResponse = Omit<
|
||||
AnalysisResultDetailsMapped,
|
||||
@@ -29,7 +29,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1744-2',
|
||||
},
|
||||
},
|
||||
@@ -49,7 +49,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1920-8',
|
||||
},
|
||||
},
|
||||
@@ -69,7 +69,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1988-5',
|
||||
},
|
||||
},
|
||||
@@ -89,7 +89,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '57747-8',
|
||||
},
|
||||
},
|
||||
@@ -109,7 +109,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '2276-4',
|
||||
},
|
||||
},
|
||||
@@ -129,10 +129,30 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14771-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '59156-0',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Glükoos',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 2,
|
||||
normStatus: 2,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: false,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '59156-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '59156-0',
|
||||
isWaitingForResults: false,
|
||||
@@ -146,10 +166,10 @@ const big1: AnalysisTestResponse = {
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: false,
|
||||
responseValueIsWithinNorm: true,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '59156-0',
|
||||
},
|
||||
},
|
||||
@@ -169,7 +189,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '13955-0',
|
||||
},
|
||||
},
|
||||
@@ -189,7 +209,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14646-4',
|
||||
},
|
||||
},
|
||||
@@ -209,7 +229,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '2000-8',
|
||||
},
|
||||
},
|
||||
@@ -229,7 +249,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '59158-6',
|
||||
},
|
||||
},
|
||||
@@ -249,7 +269,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14647-2',
|
||||
},
|
||||
},
|
||||
@@ -269,7 +289,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14682-9',
|
||||
},
|
||||
},
|
||||
@@ -289,7 +309,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '22748-8',
|
||||
},
|
||||
},
|
||||
@@ -309,7 +329,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '58805-3',
|
||||
},
|
||||
},
|
||||
@@ -329,7 +349,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '2601-3',
|
||||
},
|
||||
},
|
||||
@@ -350,7 +370,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '70204-3',
|
||||
},
|
||||
},
|
||||
@@ -370,7 +390,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14798-3',
|
||||
},
|
||||
},
|
||||
@@ -391,7 +411,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14927-8',
|
||||
},
|
||||
},
|
||||
@@ -411,7 +431,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '3016-3',
|
||||
},
|
||||
},
|
||||
@@ -431,7 +451,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '22664-7',
|
||||
},
|
||||
},
|
||||
@@ -451,7 +471,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '50561-0',
|
||||
},
|
||||
},
|
||||
@@ -472,7 +492,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '60493-4',
|
||||
},
|
||||
},
|
||||
@@ -492,7 +512,7 @@ const big1: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: true,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '60025-4',
|
||||
},
|
||||
},
|
||||
@@ -518,7 +538,7 @@ const big2: AnalysisTestResponse = {
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1988-5',
|
||||
},
|
||||
},
|
||||
@@ -538,6 +558,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 150,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '718-7',
|
||||
},
|
||||
{
|
||||
@@ -550,6 +572,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 45,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '4544-3',
|
||||
},
|
||||
{
|
||||
@@ -562,6 +586,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '6690-2',
|
||||
},
|
||||
{
|
||||
@@ -574,6 +600,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '789-8',
|
||||
},
|
||||
{
|
||||
@@ -586,6 +614,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 85,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '787-2',
|
||||
},
|
||||
{
|
||||
@@ -598,6 +628,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 30,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '785-6',
|
||||
},
|
||||
{
|
||||
@@ -610,6 +642,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 355,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '786-4',
|
||||
},
|
||||
{
|
||||
@@ -622,6 +656,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 15,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '788-0',
|
||||
},
|
||||
{
|
||||
@@ -634,6 +670,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 255,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '777-3',
|
||||
},
|
||||
{
|
||||
@@ -646,6 +684,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 0.2,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '51637-7',
|
||||
},
|
||||
{
|
||||
@@ -658,6 +698,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 10,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '32623-1',
|
||||
},
|
||||
{
|
||||
@@ -670,6 +712,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 15,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '32207-3',
|
||||
},
|
||||
{
|
||||
@@ -682,6 +726,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 0.05,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '704-7',
|
||||
},
|
||||
{
|
||||
@@ -694,6 +740,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 0.05,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '711-2',
|
||||
},
|
||||
{
|
||||
@@ -706,6 +754,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '751-8',
|
||||
},
|
||||
{
|
||||
@@ -718,6 +768,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 0.5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '742-7',
|
||||
},
|
||||
{
|
||||
@@ -730,6 +782,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 1.5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '731-0',
|
||||
},
|
||||
{
|
||||
@@ -742,6 +796,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 0,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '51584-1',
|
||||
},
|
||||
{
|
||||
@@ -754,6 +810,8 @@ const big2: AnalysisTestResponse = {
|
||||
responseValue: 0,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '38518-7',
|
||||
},
|
||||
{
|
||||
@@ -762,8 +820,12 @@ const big2: AnalysisTestResponse = {
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0,
|
||||
normUpper: null,
|
||||
normLower: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '771-6',
|
||||
},
|
||||
{
|
||||
@@ -772,8 +834,12 @@ const big2: AnalysisTestResponse = {
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0,
|
||||
normUpper: null,
|
||||
normLower: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '58413-6',
|
||||
},
|
||||
],
|
||||
@@ -787,7 +853,7 @@ const big2: AnalysisTestResponse = {
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: false,
|
||||
responseValueIsWithinNorm: false,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '57021-8',
|
||||
},
|
||||
},
|
||||
@@ -808,7 +874,7 @@ const big2: AnalysisTestResponse = {
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: false,
|
||||
responseValueIsWithinNorm: false,
|
||||
status: '5',
|
||||
status: 5,
|
||||
analysisElementOriginalId: '43583-4',
|
||||
},
|
||||
},
|
||||
@@ -830,7 +896,7 @@ const big2: AnalysisTestResponse = {
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
status: '4',
|
||||
status: 4,
|
||||
analysisElementOriginalId: '60493-4',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,46 +1,52 @@
|
||||
'use server';
|
||||
|
||||
import { MontonioOrderToken } from '@/app/home/(user)/_components/cart/types';
|
||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||
import { placeOrder, retrieveCart } from '@lib/data/cart';
|
||||
import { listProductTypes } from '@lib/data/products';
|
||||
import type { StoreOrder } from '@medusajs/types';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { z } from "zod";
|
||||
import { MontonioOrderToken } from "@/app/home/(user)/_components/cart/types";
|
||||
import { loadCurrentUserAccount } from "@/app/home/(user)/_lib/server/load-user-account";
|
||||
import { listProductTypes } from "@lib/data/products";
|
||||
import { placeOrder, retrieveCart } from "@lib/data/cart";
|
||||
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
||||
import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service';
|
||||
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
||||
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import { createNotificationsApi } from '@kit/notifications/api';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import type { StoreOrder } from '@medusajs/types';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
||||
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||
import {
|
||||
createAnalysisOrder,
|
||||
getAnalysisOrder,
|
||||
} from '~/lib/services/order.service';
|
||||
|
||||
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
||||
const ANALYSIS_TYPE_HANDLE = 'synlab-analysis';
|
||||
const MONTONIO_PAID_STATUS = 'PAID';
|
||||
|
||||
const env = () => z
|
||||
.object({
|
||||
emailSender: z
|
||||
.string({
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1),
|
||||
siteUrl: z
|
||||
.string({
|
||||
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
})
|
||||
.min(1),
|
||||
isEnabledDispatchOnMontonioCallback: z
|
||||
.boolean({
|
||||
const env = () =>
|
||||
z
|
||||
.object({
|
||||
emailSender: z
|
||||
.string({
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1),
|
||||
siteUrl: z
|
||||
.string({
|
||||
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
})
|
||||
.min(1),
|
||||
isEnabledDispatchOnMontonioCallback: z.boolean({
|
||||
error: 'MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK is required',
|
||||
}),
|
||||
})
|
||||
.parse({
|
||||
emailSender: process.env.EMAIL_SENDER,
|
||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||
isEnabledDispatchOnMontonioCallback: process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
|
||||
});
|
||||
})
|
||||
.parse({
|
||||
emailSender: process.env.EMAIL_SENDER,
|
||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||
isEnabledDispatchOnMontonioCallback:
|
||||
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
|
||||
});
|
||||
|
||||
const sendEmail = async ({
|
||||
account,
|
||||
@@ -49,15 +55,17 @@ const sendEmail = async ({
|
||||
partnerLocationName,
|
||||
language,
|
||||
}: {
|
||||
account: Pick<AccountWithParams, 'name' | 'id'>,
|
||||
email: string,
|
||||
analysisPackageName: string,
|
||||
partnerLocationName: string,
|
||||
language: string,
|
||||
account: Pick<AccountWithParams, 'name' | 'id'>;
|
||||
email: string;
|
||||
analysisPackageName: string;
|
||||
partnerLocationName: string;
|
||||
language: string;
|
||||
}) => {
|
||||
const client = getSupabaseServerAdminClient();
|
||||
try {
|
||||
const { renderSynlabAnalysisPackageEmail } = await import('@kit/email-templates');
|
||||
const { renderSynlabAnalysisPackageEmail } = await import(
|
||||
'@kit/email-templates'
|
||||
);
|
||||
const { getMailer } = await import('@kit/mailers');
|
||||
|
||||
const mailer = await getMailer();
|
||||
@@ -79,15 +87,14 @@ const sendEmail = async ({
|
||||
.catch((error) => {
|
||||
throw new Error(`Failed to send email, message=${error}`);
|
||||
});
|
||||
await createNotificationsApi(client)
|
||||
.createNotification({
|
||||
account_id: account.id,
|
||||
body: html,
|
||||
});
|
||||
await createNotificationsApi(client).createNotification({
|
||||
account_id: account.id,
|
||||
body: html,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send email, message=${error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function decodeOrderToken(orderToken: string) {
|
||||
const secretKey = process.env.MONTONIO_SECRET_KEY as string;
|
||||
@@ -97,7 +104,7 @@ async function decodeOrderToken(orderToken: string) {
|
||||
}) as MontonioOrderToken;
|
||||
|
||||
if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) {
|
||||
throw new Error("Payment not successful");
|
||||
throw new Error('Payment not successful');
|
||||
}
|
||||
|
||||
return decoded;
|
||||
@@ -106,38 +113,49 @@ async function decodeOrderToken(orderToken: string) {
|
||||
async function getCartByOrderToken(decoded: MontonioOrderToken) {
|
||||
const [, , cartId] = decoded.merchantReferenceDisplay.split(':');
|
||||
if (!cartId) {
|
||||
throw new Error("Cart ID not found");
|
||||
throw new Error('Cart ID not found');
|
||||
}
|
||||
const cart = await retrieveCart(cartId);
|
||||
if (!cart) {
|
||||
throw new Error("Cart not found");
|
||||
throw new Error('Cart not found');
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
|
||||
async function getOrderResultParameters(medusaOrder: StoreOrder) {
|
||||
const { productTypes } = await listProductTypes();
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
|
||||
const analysisType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_TYPE_HANDLE);
|
||||
const analysisPackagesType = productTypes.find(
|
||||
({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE,
|
||||
);
|
||||
const analysisType = productTypes.find(
|
||||
({ metadata }) => metadata?.handle === ANALYSIS_TYPE_HANDLE,
|
||||
);
|
||||
|
||||
const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id);
|
||||
const analysisItems = medusaOrder.items?.filter(({ product_type_id }) => product_type_id === analysisType?.id);
|
||||
const analysisPackageOrderItem = medusaOrder.items?.find(
|
||||
({ product_type_id }) => product_type_id === analysisPackagesType?.id,
|
||||
);
|
||||
const analysisItems = medusaOrder.items?.filter(
|
||||
({ product_type_id }) => product_type_id === analysisType?.id,
|
||||
);
|
||||
|
||||
return {
|
||||
medusaOrderId: medusaOrder.id,
|
||||
email: medusaOrder.email,
|
||||
analysisPackageOrder: analysisPackageOrderItem
|
||||
? {
|
||||
partnerLocationName: analysisPackageOrderItem?.metadata?.partner_location_name as string ?? '',
|
||||
analysisPackageName: analysisPackageOrderItem?.title ?? '',
|
||||
}
|
||||
: null,
|
||||
analysisItemsOrder: Array.isArray(analysisItems) && analysisItems.length > 0
|
||||
? analysisItems.map(({ product }) => ({
|
||||
analysisName: product?.title ?? '',
|
||||
analysisId: product?.metadata?.analysisIdOriginal as string ?? '',
|
||||
}))
|
||||
partnerLocationName:
|
||||
(analysisPackageOrderItem?.metadata
|
||||
?.partner_location_name as string) ?? '',
|
||||
analysisPackageName: analysisPackageOrderItem?.title ?? '',
|
||||
}
|
||||
: null,
|
||||
analysisItemsOrder:
|
||||
Array.isArray(analysisItems) && analysisItems.length > 0
|
||||
? analysisItems.map(({ product }) => ({
|
||||
analysisName: product?.title ?? '',
|
||||
analysisId: (product?.metadata?.analysisIdOriginal as string) ?? '',
|
||||
}))
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,12 +164,12 @@ async function sendAnalysisPackageOrderEmail({
|
||||
email,
|
||||
analysisPackageOrder,
|
||||
}: {
|
||||
account: AccountWithParams,
|
||||
email: string,
|
||||
account: AccountWithParams;
|
||||
email: string;
|
||||
analysisPackageOrder: {
|
||||
partnerLocationName: string,
|
||||
analysisPackageName: string,
|
||||
},
|
||||
partnerLocationName: string;
|
||||
analysisPackageName: string;
|
||||
};
|
||||
}) {
|
||||
const { language } = await createI18nServerInstance();
|
||||
const { analysisPackageName, partnerLocationName } = analysisPackageOrder;
|
||||
@@ -163,52 +181,74 @@ async function sendAnalysisPackageOrderEmail({
|
||||
partnerLocationName,
|
||||
language,
|
||||
});
|
||||
console.info(`Successfully sent analysis package order email to ${email}`);
|
||||
} catch (error) {
|
||||
console.error("Failed to send email", error);
|
||||
console.error(
|
||||
`Failed to send analysis package order email to ${email}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function processMontonioCallback(orderToken: string) {
|
||||
const { account } = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error("Account not found in context");
|
||||
throw new Error('Account not found in context');
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = await decodeOrderToken(orderToken);
|
||||
const cart = await getCartByOrderToken(decoded);
|
||||
|
||||
const medusaOrder = await placeOrder(cart.id, { revalidateCacheTags: false });
|
||||
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
|
||||
const medusaOrder = await placeOrder(cart.id, {
|
||||
revalidateCacheTags: false,
|
||||
});
|
||||
const orderedAnalysisElements = await getOrderedAnalysisIds({
|
||||
medusaOrder,
|
||||
});
|
||||
|
||||
try {
|
||||
const existingAnalysisOrder = await getAnalysisOrder({ medusaOrderId: medusaOrder.id });
|
||||
console.info(`Analysis order already exists for medusaOrderId=${medusaOrder.id}, orderId=${existingAnalysisOrder.id}`);
|
||||
const existingAnalysisOrder = await getAnalysisOrder({
|
||||
medusaOrderId: medusaOrder.id,
|
||||
});
|
||||
console.info(
|
||||
`Analysis order already exists for medusaOrderId=${medusaOrder.id}, orderId=${existingAnalysisOrder.id}`,
|
||||
);
|
||||
return { success: true, orderId: existingAnalysisOrder.id };
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
const orderId = await createAnalysisOrder({ medusaOrder, orderedAnalysisElements });
|
||||
const orderId = await createAnalysisOrder({
|
||||
medusaOrder,
|
||||
orderedAnalysisElements,
|
||||
});
|
||||
const orderResult = await getOrderResultParameters(medusaOrder);
|
||||
|
||||
const { medusaOrderId, email, analysisPackageOrder, analysisItemsOrder } = orderResult;
|
||||
const { medusaOrderId, email, analysisPackageOrder, analysisItemsOrder } =
|
||||
orderResult;
|
||||
|
||||
if (email) {
|
||||
if (analysisPackageOrder) {
|
||||
await sendAnalysisPackageOrderEmail({ account, email, analysisPackageOrder });
|
||||
await sendAnalysisPackageOrderEmail({
|
||||
account,
|
||||
email,
|
||||
analysisPackageOrder,
|
||||
});
|
||||
} else {
|
||||
console.info(`Order has no analysis package, skipping email.`);
|
||||
}
|
||||
|
||||
if (analysisItemsOrder) {
|
||||
// @TODO send email for separate analyses
|
||||
console.warn(`Order has analysis items, but no email template exists yet`);
|
||||
console.warn(
|
||||
`Order has analysis items, but no email template exists yet`,
|
||||
);
|
||||
} else {
|
||||
console.info(`Order has no analysis items, skipping email.`);
|
||||
}
|
||||
} else {
|
||||
console.error("Missing email to send order result email", orderResult);
|
||||
console.error('Missing email to send order result email', orderResult);
|
||||
}
|
||||
|
||||
if (env().isEnabledDispatchOnMontonioCallback) {
|
||||
@@ -217,7 +257,7 @@ export async function processMontonioCallback(orderToken: string) {
|
||||
|
||||
return { success: true, orderId };
|
||||
} catch (error) {
|
||||
console.error("Failed to place order", error);
|
||||
console.error('Failed to place order', error);
|
||||
throw new Error(`Failed to place order, message=${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { processMontonioCallback } from './actions';
|
||||
|
||||
export default function MontonioCallbackClient({ orderToken, error }: {
|
||||
export default function MontonioCallbackClient({
|
||||
orderToken,
|
||||
error,
|
||||
}: {
|
||||
orderToken?: string;
|
||||
error?: string;
|
||||
}) {
|
||||
@@ -32,7 +37,7 @@ export default function MontonioCallbackClient({ orderToken, error }: {
|
||||
const { orderId } = await processMontonioCallback(orderToken);
|
||||
router.push(`/home/order/${orderId}/confirmed`);
|
||||
} catch (error) {
|
||||
console.error("Failed to place order", error);
|
||||
console.error('Failed to place order', error);
|
||||
router.push('/home/cart/montonio-callback/error');
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
@@ -43,9 +48,9 @@ export default function MontonioCallbackClient({ orderToken, error }: {
|
||||
}, [orderToken, error, router, hasProcessed, isProcessing]);
|
||||
|
||||
return (
|
||||
<div className="flex mt-10 justify-center min-h-screen">
|
||||
<div className="mt-10 flex min-h-screen justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<div className="border-primary mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-b-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
|
||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Alert, AlertDescription } from '@kit/ui/shadcn/alert';
|
||||
import { AlertTitle } from '@kit/ui/shadcn/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import MontonioCallbackClient from './client-component';
|
||||
|
||||
export default async function MontonioCallbackPage({ searchParams }: {
|
||||
export default async function MontonioCallbackPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{
|
||||
'order-token'?: string;
|
||||
}>;
|
||||
}) {
|
||||
const orderToken = (await searchParams)['order-token'];
|
||||
|
||||
|
||||
if (!orderToken) {
|
||||
return <MontonioCallbackClient error="Order token is missing" />;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
|
||||
import { retrieveCart } from '@lib/data/cart';
|
||||
import Cart from '../../_components/cart';
|
||||
import { listProductTypes } from '@lib/data/products';
|
||||
import CartTimer from '../../_components/cart/cart-timer';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import Cart from '../../_components/cart';
|
||||
import CartTimer from '../../_components/cart/cart-timer';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
@@ -20,34 +22,51 @@ export async function generateMetadata() {
|
||||
|
||||
async function CartPage() {
|
||||
const cart = await retrieveCart().catch((error) => {
|
||||
console.error("Failed to retrieve cart", error);
|
||||
console.error('Failed to retrieve cart', error);
|
||||
return notFound();
|
||||
});
|
||||
|
||||
const { productTypes } = await listProductTypes();
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
const synlabAnalysisType = productTypes.find(({ metadata }) => metadata?.handle === 'synlab-analysis');
|
||||
const synlabAnalyses = analysisPackagesType && synlabAnalysisType && cart?.items
|
||||
? cart.items.filter((item) => {
|
||||
const productTypeId = item.product?.type_id;
|
||||
if (!productTypeId) {
|
||||
return false;
|
||||
}
|
||||
return [analysisPackagesType.id, synlabAnalysisType.id].includes(productTypeId);
|
||||
})
|
||||
: [];
|
||||
const ttoServiceItems = cart?.items?.filter((item) => !synlabAnalyses.some((analysis) => analysis.id === item.id)) ?? [];
|
||||
const analysisPackagesType = productTypes.find(
|
||||
({ metadata }) => metadata?.handle === 'analysis-packages',
|
||||
);
|
||||
const synlabAnalysisType = productTypes.find(
|
||||
({ metadata }) => metadata?.handle === 'synlab-analysis',
|
||||
);
|
||||
const synlabAnalyses =
|
||||
analysisPackagesType && synlabAnalysisType && cart?.items
|
||||
? cart.items.filter((item) => {
|
||||
const productTypeId = item.product?.type_id;
|
||||
if (!productTypeId) {
|
||||
return false;
|
||||
}
|
||||
return [analysisPackagesType.id, synlabAnalysisType.id].includes(
|
||||
productTypeId,
|
||||
);
|
||||
})
|
||||
: [];
|
||||
const ttoServiceItems =
|
||||
cart?.items?.filter(
|
||||
(item) => !synlabAnalyses.some((analysis) => analysis.id === item.id),
|
||||
) ?? [];
|
||||
|
||||
const otherItemsSorted = ttoServiceItems.sort((a, b) => (a.updated_at ?? "") > (b.updated_at ?? "") ? -1 : 1);
|
||||
const otherItemsSorted = ttoServiceItems.sort((a, b) =>
|
||||
(a.updated_at ?? '') > (b.updated_at ?? '') ? -1 : 1,
|
||||
);
|
||||
const item = otherItemsSorted[0];
|
||||
const isTimerShown = ttoServiceItems.length > 0 && !!item && !!item.updated_at;
|
||||
const isTimerShown =
|
||||
ttoServiceItems.length > 0 && !!item && !!item.updated_at;
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<PageHeader title={<Trans i18nKey="cart:title" />}>
|
||||
{isTimerShown && <CartTimer cartItem={item} />}
|
||||
</PageHeader>
|
||||
<Cart cart={cart} synlabAnalyses={synlabAnalyses} ttoServiceItems={ttoServiceItems} />
|
||||
<Cart
|
||||
cart={cart}
|
||||
synlabAnalyses={synlabAnalyses}
|
||||
ttoServiceItems={ttoServiceItems}
|
||||
/>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Scale } from 'lucide-react';
|
||||
|
||||
import SelectAnalysisPackages from '@kit/shared/components/select-analysis-packages';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import SelectAnalysisPackages from '@kit/shared/components/select-analysis-packages';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import ComparePackagesModal from '../../_components/compare-packages-modal';
|
||||
import { loadAnalysisPackages } from '../../_lib/server/load-analysis-packages';
|
||||
|
||||
@@ -21,7 +21,8 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
async function OrderAnalysisPackagePage() {
|
||||
const { analysisPackageElements, analysisPackages, countryCode } = await loadAnalysisPackages();
|
||||
const { analysisPackageElements, analysisPackages, countryCode } =
|
||||
await loadAnalysisPackages();
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
@@ -40,7 +41,10 @@ async function OrderAnalysisPackagePage() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SelectAnalysisPackages analysisPackages={analysisPackages} countryCode={countryCode} />
|
||||
<SelectAnalysisPackages
|
||||
analysisPackages={analysisPackages}
|
||||
countryCode={countryCode}
|
||||
/>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import {
|
||||
PageViewAction,
|
||||
createPageViewLog,
|
||||
} from '~/lib/services/audit/pageView.service';
|
||||
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
||||
import { createPageViewLog, PageViewAction } from '~/lib/services/audit/pageView.service';
|
||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
@@ -24,7 +28,7 @@ async function OrderAnalysisPage() {
|
||||
}
|
||||
|
||||
const { analyses, countryCode } = await loadAnalyses();
|
||||
|
||||
|
||||
await createPageViewLog({
|
||||
accountId: account.id,
|
||||
action: PageViewAction.VIEW_ORDER_ANALYSIS,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
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 { getAnalysisOrder } from '~/lib/services/order.service';
|
||||
import { retrieveOrder } from '@lib/data/orders';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import Divider from "@modules/common/components/divider"
|
||||
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
||||
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 { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||
import { retrieveOrder } from '@lib/data/orders';
|
||||
import Divider from '@modules/common/components/divider';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { getAnalysisOrder } from '~/lib/services/order.service';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
@@ -27,12 +27,16 @@ async function OrderConfirmedPage(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const order = await getAnalysisOrder({ analysisOrderId: Number(params.orderId) }).catch(() => null);
|
||||
const order = await getAnalysisOrder({
|
||||
analysisOrderId: Number(params.orderId),
|
||||
}).catch(() => null);
|
||||
if (!order) {
|
||||
redirect(pathsConfig.app.myOrders);
|
||||
}
|
||||
|
||||
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
|
||||
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(
|
||||
() => null,
|
||||
);
|
||||
if (!medusaOrder) {
|
||||
redirect(pathsConfig.app.myOrders);
|
||||
}
|
||||
@@ -41,7 +45,7 @@ async function OrderConfirmedPage(props: {
|
||||
<PageBody>
|
||||
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
|
||||
<Divider />
|
||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
|
||||
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 gap-y-6 lg:px-4">
|
||||
<OrderDetails order={order} />
|
||||
<Divider />
|
||||
<OrderItems medusaOrder={medusaOrder} />
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
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 { getAnalysisOrder } from '~/lib/services/order.service';
|
||||
import { retrieveOrder } from '@lib/data/orders';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import Divider from "@modules/common/components/divider"
|
||||
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
||||
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 { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||
import { retrieveOrder } from '@lib/data/orders';
|
||||
import Divider from '@modules/common/components/divider';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { getAnalysisOrder } from '~/lib/services/order.service';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
@@ -27,12 +27,16 @@ async function OrderConfirmedPage(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const order = await getAnalysisOrder({ analysisOrderId: Number(params.orderId) }).catch(() => null);
|
||||
const order = await getAnalysisOrder({
|
||||
analysisOrderId: Number(params.orderId),
|
||||
}).catch(() => null);
|
||||
if (!order) {
|
||||
redirect(pathsConfig.app.myOrders);
|
||||
}
|
||||
|
||||
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
|
||||
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(
|
||||
() => null,
|
||||
);
|
||||
if (!medusaOrder) {
|
||||
redirect(pathsConfig.app.myOrders);
|
||||
}
|
||||
@@ -41,7 +45,7 @@ async function OrderConfirmedPage(props: {
|
||||
<PageBody>
|
||||
<PageHeader title={<Trans i18nKey="cart:order.title" />} />
|
||||
<Divider />
|
||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
|
||||
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 gap-y-6 lg:px-4">
|
||||
<OrderDetails order={order} />
|
||||
<Divider />
|
||||
<OrderItems medusaOrder={medusaOrder} />
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
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 { PageBody } from '@kit/ui/makerkit/page';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
import { getAnalysisOrders } from '~/lib/services/order.service';
|
||||
import OrderBlock from '../../_components/orders/order-block';
|
||||
import React from 'react';
|
||||
import { Divider } from '@medusajs/ui';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { PageBody } from '@kit/ui/makerkit/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { getAnalysisOrders } from '~/lib/services/order.service';
|
||||
import { listOrders } from '~/medusa/lib/data/orders';
|
||||
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
import OrderBlock from '../../_components/orders/order-block';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
@@ -31,7 +34,9 @@ async function OrdersPage() {
|
||||
redirect(pathsConfig.auth.signIn);
|
||||
}
|
||||
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages')!;
|
||||
const analysisPackagesType = productTypes.find(
|
||||
({ metadata }) => metadata?.handle === 'analysis-packages',
|
||||
)!;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -41,14 +46,20 @@ async function OrdersPage() {
|
||||
/>
|
||||
<PageBody>
|
||||
{analysisOrders.map((analysisOrder) => {
|
||||
const medusaOrder = medusaOrders.find(({ id }) => id === analysisOrder.medusa_order_id);
|
||||
const medusaOrder = medusaOrders.find(
|
||||
({ id }) => id === analysisOrder.medusa_order_id,
|
||||
);
|
||||
if (!medusaOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const medusaOrderItems = medusaOrder.items || [];
|
||||
const medusaOrderItemsAnalysisPackages = medusaOrderItems.filter((item) => item.product_type_id === analysisPackagesType?.id);
|
||||
const medusaOrderItemsOther = medusaOrderItems.filter((item) => item.product_type_id !== analysisPackagesType?.id);
|
||||
const medusaOrderItemsAnalysisPackages = medusaOrderItems.filter(
|
||||
(item) => item.product_type_id === analysisPackagesType?.id,
|
||||
);
|
||||
const medusaOrderItemsOther = medusaOrderItems.filter(
|
||||
(item) => item.product_type_id !== analysisPackagesType?.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={analysisOrder.id}>
|
||||
@@ -59,7 +70,7 @@ async function OrdersPage() {
|
||||
itemsOther={medusaOrderItemsOther}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
})}
|
||||
{analysisOrders.length === 0 && (
|
||||
<h5 className="mt-6">
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Suspense } from 'react';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { toTitleCase } from '@/lib/utils';
|
||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Skeleton } from '@kit/ui/skeleton';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { createUserAnalysesApi } from '@kit/user-analyses/api';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { StoreCart, StoreCartLineItem } from "@medusajs/types"
|
||||
import { Form } from "@kit/ui/form";
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Form } from '@kit/ui/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -17,29 +17,39 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kit/ui/select';
|
||||
import { updateCartPartnerLocation } from '../../_lib/server/update-cart-partner-location';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { updateCartPartnerLocation } from '../../_lib/server/update-cart-partner-location';
|
||||
import partnerLocations from './partner-locations.json';
|
||||
|
||||
const AnalysisLocationSchema = z.object({
|
||||
locationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: StoreCart, synlabAnalyses: StoreCartLineItem[] }) {
|
||||
export default function AnalysisLocation({
|
||||
cart,
|
||||
synlabAnalyses,
|
||||
}: {
|
||||
cart: StoreCart;
|
||||
synlabAnalyses: StoreCartLineItem[];
|
||||
}) {
|
||||
const { t } = useTranslation('cart');
|
||||
|
||||
const form = useForm<z.infer<typeof AnalysisLocationSchema>>({
|
||||
defaultValues: {
|
||||
locationId: cart.metadata?.partner_location_id as string ?? '',
|
||||
locationId: (cart.metadata?.partner_location_id as string) ?? '',
|
||||
},
|
||||
resolver: zodResolver(AnalysisLocationSchema),
|
||||
});
|
||||
|
||||
const getLocation = (locationId: string) => partnerLocations.find(({ name }) => name === locationId);
|
||||
const getLocation = (locationId: string) =>
|
||||
partnerLocations.find(({ name }) => name === locationId);
|
||||
|
||||
const selectedLocation = getLocation(form.watch('locationId'));
|
||||
|
||||
const onSubmit = async ({ locationId }: z.infer<typeof AnalysisLocationSchema>) => {
|
||||
const onSubmit = async ({
|
||||
locationId,
|
||||
}: z.infer<typeof AnalysisLocationSchema>) => {
|
||||
const promise = updateCartPartnerLocation({
|
||||
cartId: cart.id,
|
||||
lineIds: synlabAnalyses.map(({ id }) => id),
|
||||
@@ -52,18 +62,18 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
|
||||
loading: t(`cart:items.analysisLocation.loading`),
|
||||
error: t(`cart:items.analysisLocation.error`),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="txt-medium flex h-full w-full flex-col gap-y-4 bg-white">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey={'cart:locations.description'} />
|
||||
</p>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => onSubmit(data))}
|
||||
className="w-full mb-2 flex gap-x-2 flex-1"
|
||||
className="mb-2 flex w-full flex-1 gap-x-2"
|
||||
>
|
||||
<Select
|
||||
value={form.watch('locationId')}
|
||||
@@ -82,34 +92,38 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{Object.entries(partnerLocations
|
||||
.reduce((acc, curr) => ({
|
||||
...acc,
|
||||
[curr.city]: [...((acc[curr.city] as typeof partnerLocations) ?? []), curr],
|
||||
}), {} as Record<string, typeof partnerLocations>))
|
||||
.map(([city, locations]) => (
|
||||
<SelectGroup key={city}>
|
||||
<SelectLabel>{city}</SelectLabel>
|
||||
{locations.map((location) => (
|
||||
<SelectItem key={location.name} value={location.name}>{location.name}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
{Object.entries(
|
||||
partnerLocations.reduce(
|
||||
(acc, curr) => ({
|
||||
...acc,
|
||||
[curr.city]: [
|
||||
...((acc[curr.city] as typeof partnerLocations) ?? []),
|
||||
curr,
|
||||
],
|
||||
}),
|
||||
{} as Record<string, typeof partnerLocations>,
|
||||
),
|
||||
).map(([city, locations]) => (
|
||||
<SelectGroup key={city}>
|
||||
<SelectLabel>{city}</SelectLabel>
|
||||
{locations.map((location) => (
|
||||
<SelectItem key={location.name} value={location.name}>
|
||||
{location.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{selectedLocation && (
|
||||
<div className="flex flex-col gap-y-2 mb-4">
|
||||
<p className="text-sm">
|
||||
{selectedLocation.address}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{selectedLocation.hours}
|
||||
</p>
|
||||
<div className="mb-4 flex flex-col gap-y-2">
|
||||
<p className="text-sm">{selectedLocation.address}</p>
|
||||
<p className="text-sm">{selectedLocation.hours}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Spinner } from '@medusajs/icons';
|
||||
import { Trash } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Spinner } from "@medusajs/icons";
|
||||
import { handleDeleteCartItem } from "~/lib/services/medusaCart.service";
|
||||
import { handleDeleteCartItem } from '~/lib/services/medusaCart.service';
|
||||
|
||||
const CartItemDelete = ({
|
||||
id,
|
||||
@@ -33,9 +34,9 @@ const CartItemDelete = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between text-small-regular">
|
||||
<div className="text-small-regular flex items-center justify-between">
|
||||
<button
|
||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
||||
className="text-ui-fg-subtle hover:text-ui-fg-base flex cursor-pointer gap-x-1"
|
||||
onClick={() => handleDelete()}
|
||||
>
|
||||
{isDeleting ? <Spinner className="animate-spin" /> : <Trash />}
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from '@kit/ui/table';
|
||||
import { formatCurrency } from "@/packages/shared/src/utils"
|
||||
import CartItemDelete from "./cart-item-delete";
|
||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||
import { HttpTypes } from '@medusajs/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function CartItem({ item, currencyCode }: {
|
||||
item: HttpTypes.StoreCartLineItem
|
||||
currencyCode: string
|
||||
import { TableCell, TableRow } from '@kit/ui/table';
|
||||
|
||||
import CartItemDelete from './cart-item-delete';
|
||||
|
||||
export default function CartItem({
|
||||
item,
|
||||
currencyCode,
|
||||
}: {
|
||||
item: HttpTypes.StoreCartLineItem;
|
||||
currencyCode: string;
|
||||
}) {
|
||||
const { i18n: { language } } = useTranslation();
|
||||
const {
|
||||
i18n: { language },
|
||||
} = useTranslation();
|
||||
|
||||
return (
|
||||
<TableRow className="w-full" data-testid="product-row">
|
||||
<TableCell className="text-left w-[100%] px-4 sm:px-6">
|
||||
<TableCell className="w-[100%] px-4 text-left sm:px-6">
|
||||
<p
|
||||
className="txt-medium-plus text-ui-fg-base"
|
||||
data-testid="product-title"
|
||||
@@ -26,9 +30,7 @@ export default function CartItem({ item, currencyCode }: {
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-4 sm:px-6">
|
||||
{item.quantity}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 sm:px-6">{item.quantity}</TableCell>
|
||||
|
||||
<TableCell className="min-w-[80px] px-4 sm:px-6">
|
||||
{formatCurrency({
|
||||
@@ -38,7 +40,7 @@ export default function CartItem({ item, currencyCode }: {
|
||||
})}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="min-w-[80px] px-4 sm:px-6 text-right">
|
||||
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
||||
{formatCurrency({
|
||||
value: item.total,
|
||||
currencyCode,
|
||||
@@ -46,11 +48,11 @@ export default function CartItem({ item, currencyCode }: {
|
||||
})}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right px-4 sm:px-6">
|
||||
<span className="flex gap-x-1 justify-end w-[60px]">
|
||||
<TableCell className="px-4 text-right sm:px-6">
|
||||
<span className="flex w-[60px] justify-end gap-x-1">
|
||||
<CartItemDelete id={item.id} />
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { StoreCart, StoreCartLineItem } from "@medusajs/types"
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import CartItem from "./cart-item";
|
||||
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@kit/ui/table';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export default function CartItems({ cart, items, productColumnLabelKey }: {
|
||||
import CartItem from './cart-item';
|
||||
|
||||
export default function CartItems({
|
||||
cart,
|
||||
items,
|
||||
productColumnLabelKey,
|
||||
}: {
|
||||
cart: StoreCart;
|
||||
items: StoreCartLineItem[];
|
||||
productColumnLabelKey: string;
|
||||
@@ -19,7 +25,7 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
|
||||
}
|
||||
|
||||
return (
|
||||
<Table className="rounded-lg border border-separate">
|
||||
<Table className="border-separate rounded-lg border">
|
||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||
<TableRow>
|
||||
<TableHead className="px-4 sm:px-6">
|
||||
@@ -28,19 +34,20 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
|
||||
<TableHead className="px-4 sm:px-6">
|
||||
<Trans i18nKey="cart:table.quantity" />
|
||||
</TableHead>
|
||||
<TableHead className="px-4 sm:px-6 min-w-[100px]">
|
||||
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
||||
<Trans i18nKey="cart:table.price" />
|
||||
</TableHead>
|
||||
<TableHead className="px-4 sm:px-6 min-w-[100px] text-right">
|
||||
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
||||
<Trans i18nKey="cart:table.total" />
|
||||
</TableHead>
|
||||
<TableHead className="px-4 sm:px-6">
|
||||
</TableHead>
|
||||
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items
|
||||
.sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1)
|
||||
.sort((a, b) =>
|
||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||
)
|
||||
.map((item) => (
|
||||
<CartItem
|
||||
key={item.id}
|
||||
@@ -50,5 +57,5 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { handleLineItemTimeout } from '@/lib/services/medusaCart.service';
|
||||
import { StoreCartLineItem } from '@medusajs/types';
|
||||
import { Timer } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -8,18 +14,17 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from "@kit/ui/alert-dialog";
|
||||
|
||||
import { Timer } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StoreCartLineItem } from '@medusajs/types';
|
||||
import { handleLineItemTimeout } from '@/lib/services/medusaCart.service';
|
||||
AlertDialogTitle,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
|
||||
const TIMEOUT_MINUTES = 15;
|
||||
|
||||
export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem }) {
|
||||
export default function CartTimer({
|
||||
cartItem,
|
||||
}: {
|
||||
cartItem: StoreCartLineItem;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [timeLeft, setTimeLeft] = useState<number | null>(null);
|
||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||
@@ -39,7 +44,9 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
||||
return () => clearInterval(interval);
|
||||
}, [updatedAt]);
|
||||
|
||||
const minutes = timeLeft ? Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)) : 0;
|
||||
const minutes = timeLeft
|
||||
? Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60))
|
||||
: 0;
|
||||
const seconds = timeLeft ? Math.floor((timeLeft % (1000 * 60)) / 1000) : 0;
|
||||
|
||||
const isTimeLeftPositive = timeLeft === null || timeLeft > 0;
|
||||
@@ -53,13 +60,16 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
||||
}, [isTimeLeftPositive, cartItem.id]);
|
||||
|
||||
if (timeLeft === null) {
|
||||
return <div className='min-h-[40px]' />;
|
||||
return <div className="min-h-[40px]" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ml-auto">
|
||||
<Button variant="outline" className="flex items-center gap-x-2 bg-accent hover:bg-accent px-4 cursor-default">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-accent hover:bg-accent flex cursor-default items-center gap-x-2 px-4"
|
||||
>
|
||||
<Timer />
|
||||
<span className="text-sm">
|
||||
{t('cart:checkout.timeLeft', {
|
||||
@@ -76,7 +86,9 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
||||
{t('cart:checkout.timeoutTitle')}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('cart:checkout.timeoutDescription', { productTitle: cartItem.product?.title })}
|
||||
{t('cart:checkout.timeoutDescription', {
|
||||
productTitle: cartItem.product?.title,
|
||||
})}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@@ -87,5 +99,5 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server"
|
||||
'use server';
|
||||
|
||||
import { applyPromotions } from "@lib/data/cart"
|
||||
import { applyPromotions } from '@lib/data/cart';
|
||||
|
||||
export async function addPromotionCodeAction(code: string) {
|
||||
try {
|
||||
@@ -12,9 +12,14 @@ export async function addPromotionCodeAction(code: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removePromotionCodeAction(codeToRemove: string, appliedCodes: string[]) {
|
||||
export async function removePromotionCodeAction(
|
||||
codeToRemove: string,
|
||||
appliedCodes: string[],
|
||||
) {
|
||||
try {
|
||||
const updatedCodes = appliedCodes.filter((appliedCode) => appliedCode !== codeToRemove);
|
||||
const updatedCodes = appliedCodes.filter(
|
||||
(appliedCode) => appliedCode !== codeToRemove,
|
||||
);
|
||||
await applyPromotions(updatedCodes);
|
||||
return { success: true, message: 'Discount code removed successfully' };
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import { Badge, Text } from "@medusajs/ui"
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { convertToLocale } from '@lib/util/money';
|
||||
import { StoreCart, StorePromotion } from '@medusajs/types';
|
||||
import { Badge, Text } from '@medusajs/ui';
|
||||
import Trash from '@modules/common/icons/trash';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { convertToLocale } from "@lib/util/money"
|
||||
import { StoreCart, StorePromotion } from "@medusajs/types"
|
||||
import Trash from "@modules/common/icons/trash"
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Form, FormControl, FormField, FormItem } from "@kit/ui/form";
|
||||
import { Form, FormControl, FormField, FormItem } from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { Input } from "@kit/ui/input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { addPromotionCodeAction, removePromotionCodeAction } from "./discount-code-actions";
|
||||
|
||||
import {
|
||||
addPromotionCodeAction,
|
||||
removePromotionCodeAction,
|
||||
} from './discount-code-actions';
|
||||
|
||||
const DiscountCodeSchema = z.object({
|
||||
code: z.string().min(1),
|
||||
})
|
||||
});
|
||||
|
||||
export default function DiscountCode({ cart }: {
|
||||
export default function DiscountCode({
|
||||
cart,
|
||||
}: {
|
||||
cart: StoreCart & {
|
||||
promotions: StorePromotion[]
|
||||
}
|
||||
promotions: StorePromotion[];
|
||||
};
|
||||
}) {
|
||||
const { t } = useTranslation('cart');
|
||||
|
||||
@@ -33,11 +40,11 @@ export default function DiscountCode({ cart }: {
|
||||
const removePromotionCode = async (code: string) => {
|
||||
const appliedCodes = promotions
|
||||
.filter((p) => p.code !== undefined)
|
||||
.map((p) => p.code!)
|
||||
.map((p) => p.code!);
|
||||
|
||||
const loading = toast.loading(t('cart:discountCode.removeLoading'));
|
||||
|
||||
const result = await removePromotionCodeAction(code, appliedCodes)
|
||||
const result = await removePromotionCodeAction(code, appliedCodes);
|
||||
|
||||
toast.dismiss(loading);
|
||||
if (result.success) {
|
||||
@@ -45,21 +52,20 @@ export default function DiscountCode({ cart }: {
|
||||
} else {
|
||||
toast.error(t('cart:discountCode.removeError'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addPromotionCode = async (code: string) => {
|
||||
const loading = toast.loading(t('cart:discountCode.addLoading'));
|
||||
const result = await addPromotionCodeAction(code)
|
||||
const result = await addPromotionCodeAction(code);
|
||||
|
||||
toast.dismiss(loading);
|
||||
if (result.success) {
|
||||
toast.success(t('cart:discountCode.addSuccess'));
|
||||
form.reset()
|
||||
form.reset();
|
||||
} else {
|
||||
toast.error(t('cart:discountCode.addError'));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const form = useForm<z.infer<typeof DiscountCodeSchema>>({
|
||||
defaultValues: {
|
||||
@@ -69,40 +75,41 @@ export default function DiscountCode({ cart }: {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="txt-medium flex h-full w-full flex-col gap-y-4 bg-white">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
||||
</p>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
||||
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2 flex-1"
|
||||
className="mb-2 flex w-full flex-1 flex-col gap-x-2 gap-y-2 sm:flex-row"
|
||||
>
|
||||
<FormField
|
||||
name={'code'}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input required type="text" {...field} placeholder={t('cart:discountCode.placeholder')} />
|
||||
<Input
|
||||
required
|
||||
type="text"
|
||||
{...field}
|
||||
placeholder={t('cart:discountCode.placeholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
className="h-min"
|
||||
>
|
||||
<Button type="submit" variant="secondary" className="h-min">
|
||||
<Trans i18nKey={'cart:discountCode.apply'} />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{promotions.length > 0 && (
|
||||
<div className="w-full flex items-center mt-4">
|
||||
<div className="flex flex-col w-full gap-y-2">
|
||||
<div className="mt-4 flex w-full items-center">
|
||||
<div className="flex w-full flex-col gap-y-2">
|
||||
<p>
|
||||
<Trans i18nKey={'cart:discountCode.appliedCodes'} />
|
||||
</p>
|
||||
@@ -111,32 +118,32 @@ export default function DiscountCode({ cart }: {
|
||||
return (
|
||||
<div
|
||||
key={promotion.id}
|
||||
className="flex items-center justify-between w-full max-w-full mb-2"
|
||||
className="mb-2 flex w-full max-w-full items-center justify-between"
|
||||
data-testid="discount-row"
|
||||
>
|
||||
<Text className="flex gap-x-1 items-baseline text-sm w-4/5 pr-1">
|
||||
<Text className="flex w-4/5 items-baseline gap-x-1 pr-1 text-sm">
|
||||
<span className="truncate" data-testid="discount-code">
|
||||
<Badge
|
||||
color={promotion.is_automatic ? "green" : "grey"}
|
||||
color={promotion.is_automatic ? 'green' : 'grey'}
|
||||
size="small"
|
||||
className="px-4 text-sm"
|
||||
>
|
||||
{promotion.code}
|
||||
</Badge>{" "}
|
||||
</Badge>{' '}
|
||||
(
|
||||
{promotion.application_method?.value !== undefined &&
|
||||
promotion.application_method.currency_code !==
|
||||
undefined && (
|
||||
undefined && (
|
||||
<>
|
||||
{promotion.application_method.type ===
|
||||
"percentage"
|
||||
{promotion.application_method.type === 'percentage'
|
||||
? `${promotion.application_method.value}%`
|
||||
: convertToLocale({
|
||||
amount: Number(promotion.application_method.value),
|
||||
currency_code:
|
||||
promotion.application_method
|
||||
.currency_code,
|
||||
})}
|
||||
amount: Number(
|
||||
promotion.application_method.value,
|
||||
),
|
||||
currency_code:
|
||||
promotion.application_method.currency_code,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
)
|
||||
@@ -152,10 +159,10 @@ export default function DiscountCode({ cart }: {
|
||||
className="flex items-center"
|
||||
onClick={() => {
|
||||
if (!promotion.code) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
removePromotionCode(promotion.code)
|
||||
removePromotionCode(promotion.code);
|
||||
}}
|
||||
data-testid="remove-discount-button"
|
||||
>
|
||||
@@ -166,11 +173,11 @@ export default function DiscountCode({ cart }: {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { handleNavigateToPayment } from '@/lib/services/medusaCart.service';
|
||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||
import { initiatePaymentSession } from '@lib/data/cart';
|
||||
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { StoreCart, StoreCartLineItem } from "@medusajs/types"
|
||||
import CartItems from "./cart-items"
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from '@kit/ui/card';
|
||||
import DiscountCode from "./discount-code";
|
||||
import { initiatePaymentSession } from "@lib/data/cart";
|
||||
import { formatCurrency } from "@/packages/shared/src/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { handleNavigateToPayment } from "@/lib/services/medusaCart.service";
|
||||
import AnalysisLocation from "./analysis-location";
|
||||
import { Card, CardContent, CardHeader } from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import AnalysisLocation from './analysis-location';
|
||||
import CartItems from './cart-items';
|
||||
import DiscountCode from './discount-code';
|
||||
|
||||
const IS_DISCOUNT_SHOWN = true as boolean;
|
||||
|
||||
@@ -25,11 +24,13 @@ export default function Cart({
|
||||
synlabAnalyses,
|
||||
ttoServiceItems,
|
||||
}: {
|
||||
cart: StoreCart | null
|
||||
cart: StoreCart | null;
|
||||
synlabAnalyses: StoreCartLineItem[];
|
||||
ttoServiceItems: StoreCartLineItem[];
|
||||
}) {
|
||||
const { i18n: { language } } = useTranslation();
|
||||
const {
|
||||
i18n: { language },
|
||||
} = useTranslation();
|
||||
|
||||
const [isInitiatingSession, setIsInitiatingSession] = useState(false);
|
||||
|
||||
@@ -39,7 +40,10 @@ export default function Cart({
|
||||
return (
|
||||
<div className="content-container py-5 lg:px-4">
|
||||
<div>
|
||||
<div className="flex flex-col justify-center items-center" data-testid="empty-cart-message">
|
||||
<div
|
||||
className="flex flex-col items-center justify-center"
|
||||
data-testid="empty-cart-message"
|
||||
>
|
||||
<h4 className="text-center">
|
||||
<Trans i18nKey="cart:emptyCartMessage" />
|
||||
</h4>
|
||||
@@ -71,21 +75,29 @@ export default function Cart({
|
||||
const isLocationsShown = synlabAnalyses.length > 0;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4">
|
||||
<div className="flex flex-col bg-white gap-y-6">
|
||||
<CartItems cart={cart} items={synlabAnalyses} productColumnLabelKey="cart:items.synlabAnalyses.productColumnLabel" />
|
||||
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
|
||||
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 lg:px-4">
|
||||
<div className="flex flex-col gap-y-6 bg-white">
|
||||
<CartItems
|
||||
cart={cart}
|
||||
items={synlabAnalyses}
|
||||
productColumnLabelKey="cart:items.synlabAnalyses.productColumnLabel"
|
||||
/>
|
||||
<CartItems
|
||||
cart={cart}
|
||||
items={ttoServiceItems}
|
||||
productColumnLabelKey="cart:items.ttoServices.productColumnLabel"
|
||||
/>
|
||||
</div>
|
||||
{hasCartItems && (
|
||||
<>
|
||||
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6 pt-2 sm:pt-4">
|
||||
<div className="w-full sm:w-auto sm:mr-[42px]">
|
||||
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||
<div className="flex gap-x-4 px-4 pt-2 sm:justify-end sm:px-6 sm:pt-4">
|
||||
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
||||
<Trans i18nKey="cart:order.subtotal" />
|
||||
</p>
|
||||
</div>
|
||||
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||
<p className="text-sm text-right">
|
||||
<p className="text-right text-sm">
|
||||
{formatCurrency({
|
||||
value: cart.subtotal,
|
||||
currencyCode: cart.currency_code,
|
||||
@@ -94,14 +106,14 @@ export default function Cart({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6 py-2 sm:py-4">
|
||||
<div className="w-full sm:w-auto sm:mr-[42px]">
|
||||
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||
<div className="flex gap-x-4 px-4 py-2 sm:justify-end sm:px-6 sm:py-4">
|
||||
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
||||
<Trans i18nKey="cart:order.promotionsTotal" />
|
||||
</p>
|
||||
</div>
|
||||
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||
<p className="text-sm text-right">
|
||||
<p className="text-right text-sm">
|
||||
{formatCurrency({
|
||||
value: cart.discount_total,
|
||||
currencyCode: cart.currency_code,
|
||||
@@ -110,14 +122,14 @@ export default function Cart({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6">
|
||||
<div className="w-full sm:w-auto sm:mr-[42px]">
|
||||
<p className="ml-0 font-bold text-sm">
|
||||
<div className="flex gap-x-4 px-4 sm:justify-end sm:px-6">
|
||||
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||
<p className="ml-0 text-sm font-bold">
|
||||
<Trans i18nKey="cart:order.total" />
|
||||
</p>
|
||||
</div>
|
||||
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||
<p className="text-sm text-right">
|
||||
<p className="text-right text-sm">
|
||||
{formatCurrency({
|
||||
value: cart.total,
|
||||
currencyCode: cart.currency_code,
|
||||
@@ -129,11 +141,9 @@ export default function Cart({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex sm:flex-row flex-col gap-y-6 py-4 sm:py-8 gap-x-4">
|
||||
<div className="flex flex-col gap-x-4 gap-y-6 py-4 sm:flex-row sm:py-8">
|
||||
{IS_DISCOUNT_SHOWN && (
|
||||
<Card
|
||||
className="flex flex-col justify-between w-full sm:w-1/2"
|
||||
>
|
||||
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
||||
<CardHeader className="pb-4">
|
||||
<h5>
|
||||
<Trans i18nKey="cart:discountCode.title" />
|
||||
@@ -146,24 +156,31 @@ export default function Cart({
|
||||
)}
|
||||
|
||||
{isLocationsShown && (
|
||||
<Card
|
||||
className="flex flex-col justify-between w-full sm:w-1/2"
|
||||
>
|
||||
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
||||
<CardHeader className="pb-4">
|
||||
<h5>
|
||||
<Trans i18nKey="cart:locations.title" />
|
||||
</h5>
|
||||
</CardHeader>
|
||||
<CardContent className="h-full">
|
||||
<AnalysisLocation cart={{ ...cart }} synlabAnalyses={synlabAnalyses} />
|
||||
<AnalysisLocation
|
||||
cart={{ ...cart }}
|
||||
synlabAnalyses={synlabAnalyses}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button className="h-10" onClick={initiatePayment} disabled={isInitiatingSession}>
|
||||
{isInitiatingSession && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||
<Button
|
||||
className="h-10"
|
||||
onClick={initiatePayment}
|
||||
disabled={isInitiatingSession}
|
||||
>
|
||||
{isInitiatingSession && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
<Trans i18nKey="cart:checkout.goToCheckout" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -119,4 +119,4 @@
|
||||
"hours": "Verevõtt tööpäeviti 8.00-12.00",
|
||||
"city": "Otepää"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -4,12 +4,12 @@ export interface MontonioOrderToken {
|
||||
merchantReference: string;
|
||||
merchantReferenceDisplay: string;
|
||||
paymentStatus:
|
||||
| 'PAID'
|
||||
| 'FAILED'
|
||||
| 'CANCELLED'
|
||||
| 'PENDING'
|
||||
| 'EXPIRED'
|
||||
| 'REFUNDED';
|
||||
| 'PAID'
|
||||
| 'FAILED'
|
||||
| 'CANCELLED'
|
||||
| 'PENDING'
|
||||
| 'EXPIRED'
|
||||
| 'REFUNDED';
|
||||
paymentMethod: string;
|
||||
grandTotal: number;
|
||||
currency: string;
|
||||
@@ -19,4 +19,4 @@ export interface MontonioOrderToken {
|
||||
paymentLinkUuid: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { JSX } from 'react';
|
||||
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
import { Check, X } from 'lucide-react';
|
||||
|
||||
import { PackageHeader } from '@kit/shared/components/package-header';
|
||||
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -18,14 +22,14 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@kit/ui/table';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { PackageHeader } from '@kit/shared/components/package-header';
|
||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export type AnalysisPackageElement = Pick<StoreProduct, 'title' | 'id' | 'description'> & {
|
||||
export type AnalysisPackageElement = Pick<
|
||||
StoreProduct,
|
||||
'title' | 'id' | 'description'
|
||||
> & {
|
||||
isIncludedInStandard: boolean;
|
||||
isIncludedInStandardPlus: boolean;
|
||||
isIncludedInPremium: boolean;
|
||||
@@ -39,7 +43,11 @@ const CheckWithBackground = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const PackageTableHead = async ({ product }: { product: AnalysisPackageWithVariant }) => {
|
||||
const PackageTableHead = async ({
|
||||
product,
|
||||
}: {
|
||||
product: AnalysisPackageWithVariant;
|
||||
}) => {
|
||||
const { t, language } = await createI18nServerInstance();
|
||||
|
||||
const { title, price, nrOfAnalyses } = product;
|
||||
@@ -48,14 +56,14 @@ const PackageTableHead = async ({ product }: { product: AnalysisPackageWithVaria
|
||||
<TableHead className="py-2">
|
||||
<PackageHeader
|
||||
title={t(title)}
|
||||
tagColor='bg-cyan'
|
||||
tagColor="bg-cyan"
|
||||
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
|
||||
language={language}
|
||||
price={price}
|
||||
/>
|
||||
</TableHead>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const ComparePackagesModal = async ({
|
||||
analysisPackages,
|
||||
@@ -69,7 +77,9 @@ const ComparePackagesModal = async ({
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
const standardPackage = analysisPackages.find(({ isStandard }) => isStandard);
|
||||
const standardPlusPackage = analysisPackages.find(({ isStandardPlus }) => isStandardPlus);
|
||||
const standardPlusPackage = analysisPackages.find(
|
||||
({ isStandardPlus }) => isStandardPlus,
|
||||
);
|
||||
const premiumPackage = analysisPackages.find(({ isPremium }) => isPremium);
|
||||
|
||||
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
|
||||
@@ -100,7 +110,7 @@ const ComparePackagesModal = async ({
|
||||
<p className="text-muted-foreground mx-auto w-3/5 text-sm">
|
||||
{t('product:healthPackageComparison.description')}
|
||||
</p>
|
||||
<div className="rounded-md border max-h-[80vh] overflow-y-auto">
|
||||
<div className="max-h-[80vh] overflow-y-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -112,16 +122,14 @@ const ComparePackagesModal = async ({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{analysisPackageElements.map(
|
||||
(
|
||||
{
|
||||
title,
|
||||
id,
|
||||
description,
|
||||
isIncludedInStandard,
|
||||
isIncludedInStandardPlus,
|
||||
isIncludedInPremium,
|
||||
},
|
||||
) => {
|
||||
({
|
||||
title,
|
||||
id,
|
||||
description,
|
||||
isIncludedInStandard,
|
||||
isIncludedInStandardPlus,
|
||||
isIncludedInPremium,
|
||||
}) => {
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
@@ -130,20 +138,28 @@ const ComparePackagesModal = async ({
|
||||
<TableRow key={id}>
|
||||
<TableCell className="py-6 sm:max-w-[30vw]">
|
||||
{title}{' '}
|
||||
{description && (<InfoTooltip content={description} icon={<QuestionMarkCircledIcon />} />)}
|
||||
{description && (
|
||||
<InfoTooltip
|
||||
content={description}
|
||||
icon={<QuestionMarkCircledIcon />}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{isIncludedInStandard && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{isIncludedInStandardPlus && <CheckWithBackground />}
|
||||
{isIncludedInStandardPlus && (
|
||||
<CheckWithBackground />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{isIncludedInPremium && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
},
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function DashboardCards() {
|
||||
<div className="flex gap-4">
|
||||
<Card
|
||||
variant="gradient-success"
|
||||
className="xs:w-1/2 sm:w-auto flex w-full flex-col justify-between"
|
||||
className="xs:w-1/2 flex w-full flex-col justify-between sm:w-auto"
|
||||
>
|
||||
<CardHeader className="flex-row">
|
||||
<div
|
||||
|
||||
@@ -3,17 +3,11 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { BlendingModeIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Droplets,
|
||||
} from 'lucide-react';
|
||||
import { Droplets } from 'lucide-react';
|
||||
|
||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from '@kit/ui/card';
|
||||
import { Card, CardContent, CardHeader } from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import { Database } from '@/packages/supabase/src/database.types';
|
||||
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import { isNil } from 'lodash';
|
||||
import {
|
||||
Activity,
|
||||
ChevronRight,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
@@ -27,13 +28,13 @@ import {
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { isNil } from 'lodash';
|
||||
import { BmiCategory } from '~/lib/types/bmi';
|
||||
import PersonalCode, {
|
||||
bmiFromMetric,
|
||||
getBmiBackgroundColor,
|
||||
getBmiStatus,
|
||||
} from '~/lib/utils';
|
||||
|
||||
import DashboardRecommendations from './dashboard-recommendations';
|
||||
|
||||
const getCardVariant = (isSuccess: boolean | null): CardProps['variant'] => {
|
||||
@@ -57,80 +58,78 @@ const cards = ({
|
||||
bmiStatus: BmiCategory | null;
|
||||
smoking?: boolean | null;
|
||||
}) => [
|
||||
{
|
||||
title: 'dashboard:gender',
|
||||
description: gender ?? '-',
|
||||
icon: <User />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:age',
|
||||
description: age ? `${age}` : '-',
|
||||
icon: <Clock9 />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:height',
|
||||
description: height ? `${height}cm` : '-',
|
||||
icon: <RulerHorizontalIcon className="size-4" />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:weight',
|
||||
description: weight ? `${weight}kg` : '-',
|
||||
icon: <Scale />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bmi',
|
||||
description: bmiFromMetric(weight || 0, height || 0)?.toString() ?? '-',
|
||||
icon: <TrendingUp />,
|
||||
iconBg: getBmiBackgroundColor(bmiStatus),
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bloodPressure',
|
||||
description: '-',
|
||||
icon: <Activity />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:cholesterol',
|
||||
description: '-',
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
iconBg: 'bg-destructive',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:ldlCholesterol',
|
||||
description: '-',
|
||||
icon: <Pill />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
// {
|
||||
// title: 'Score 2',
|
||||
// description: 'Normis',
|
||||
// icon: <LineChart />,
|
||||
// iconBg: 'bg-success',
|
||||
// },
|
||||
{
|
||||
title: 'dashboard:smoking',
|
||||
description:
|
||||
isNil(smoking)
|
||||
? 'dashboard:respondToQuestion'
|
||||
: !!smoking
|
||||
? 'common:yes'
|
||||
: 'common:no',
|
||||
descriptionColor: 'text-primary',
|
||||
icon:
|
||||
isNil(smoking) ? (
|
||||
<Link href={pathsConfig.app.personalAccountSettings}>
|
||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
) : null,
|
||||
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
|
||||
},
|
||||
];
|
||||
{
|
||||
title: 'dashboard:gender',
|
||||
description: gender ?? '-',
|
||||
icon: <User />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:age',
|
||||
description: age ? `${age}` : '-',
|
||||
icon: <Clock9 />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:height',
|
||||
description: height ? `${height}cm` : '-',
|
||||
icon: <RulerHorizontalIcon className="size-4" />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:weight',
|
||||
description: weight ? `${weight}kg` : '-',
|
||||
icon: <Scale />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bmi',
|
||||
description: bmiFromMetric(weight || 0, height || 0)?.toString() ?? '-',
|
||||
icon: <TrendingUp />,
|
||||
iconBg: getBmiBackgroundColor(bmiStatus),
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bloodPressure',
|
||||
description: '-',
|
||||
icon: <Activity />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:cholesterol',
|
||||
description: '-',
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
iconBg: 'bg-destructive',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:ldlCholesterol',
|
||||
description: '-',
|
||||
icon: <Pill />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
// {
|
||||
// title: 'Score 2',
|
||||
// description: 'Normis',
|
||||
// icon: <LineChart />,
|
||||
// iconBg: 'bg-success',
|
||||
// },
|
||||
{
|
||||
title: 'dashboard:smoking',
|
||||
description: isNil(smoking)
|
||||
? 'dashboard:respondToQuestion'
|
||||
: smoking
|
||||
? 'common:yes'
|
||||
: 'common:no',
|
||||
descriptionColor: 'text-primary',
|
||||
icon: isNil(smoking) ? (
|
||||
<Link href={pathsConfig.app.personalAccountSettings}>
|
||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
) : null,
|
||||
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
|
||||
},
|
||||
];
|
||||
|
||||
const IS_SHOWN_RECOMMENDATIONS = false as boolean;
|
||||
|
||||
@@ -146,13 +145,15 @@ export default function Dashboard({
|
||||
}) {
|
||||
const height = account.accountParams?.height || 0;
|
||||
const weight = account.accountParams?.weight || 0;
|
||||
|
||||
|
||||
let age: number = 0;
|
||||
let gender: { label: string; value: string } | null = null;
|
||||
try {
|
||||
({ age = 0, gender } = PersonalCode.parsePersonalCode(account.personal_code!));
|
||||
({ age = 0, gender } = PersonalCode.parsePersonalCode(
|
||||
account.personal_code!,
|
||||
));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse personal code", e);
|
||||
console.error('Failed to parse personal code', e);
|
||||
}
|
||||
const bmiStatus = getBmiStatus(bmiThresholds, { age, height, weight });
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@ import { useContext } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||
import { SidebarContext } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { pathsConfig, featureFlagsConfig } from '@kit/shared/config';
|
||||
|
||||
|
||||
const features = {
|
||||
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ export function HomeLayoutPageHeader(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<PageHeader description={props.description} title={props.title}>{props.children}</PageHeader>
|
||||
<PageHeader description={props.description} title={props.title}>
|
||||
{props.children}
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardDescription,
|
||||
} from '@kit/ui/card';
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
import { useState } from 'react';
|
||||
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardDescription, CardFooter, CardHeader } from '@kit/ui/card';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
||||
|
||||
export type OrderAnalysisCard = Pick<
|
||||
StoreProduct, 'title' | 'description' | 'subtitle'
|
||||
StoreProduct,
|
||||
'title' | 'description' | 'subtitle'
|
||||
> & {
|
||||
variant: { id: string };
|
||||
price: number | null;
|
||||
@@ -32,10 +30,13 @@ export default function OrderAnalysesCards({
|
||||
analyses: OrderAnalysisCard[];
|
||||
countryCode: string;
|
||||
}) {
|
||||
const {
|
||||
i18n: { language },
|
||||
} = useTranslation();
|
||||
|
||||
const { i18n: { language } } = useTranslation()
|
||||
|
||||
const [variantAddingToCart, setVariantAddingToCart] = useState<string | null>(null);
|
||||
const [variantAddingToCart, setVariantAddingToCart] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const handleSelect = async (variantId: string) => {
|
||||
if (variantAddingToCart) {
|
||||
return null;
|
||||
@@ -54,24 +55,19 @@ export default function OrderAnalysesCards({
|
||||
setVariantAddingToCart(null);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid xs:grid-cols-3 gap-6 mt-4">
|
||||
{analyses.map(({
|
||||
title,
|
||||
variant,
|
||||
description,
|
||||
subtitle,
|
||||
price,
|
||||
}) => {
|
||||
const formattedPrice = typeof price === 'number'
|
||||
? formatCurrency({
|
||||
currencyCode: 'eur',
|
||||
locale: language,
|
||||
value: price,
|
||||
})
|
||||
: null;
|
||||
<div className="xs:grid-cols-3 mt-4 grid gap-6">
|
||||
{analyses.map(({ title, variant, description, subtitle, price }) => {
|
||||
const formattedPrice =
|
||||
typeof price === 'number'
|
||||
? formatCurrency({
|
||||
currencyCode: 'eur',
|
||||
locale: language,
|
||||
value: price,
|
||||
})
|
||||
: null;
|
||||
return (
|
||||
<Card
|
||||
key={title}
|
||||
@@ -80,23 +76,29 @@ export default function OrderAnalysesCards({
|
||||
>
|
||||
<CardHeader className="flex-row">
|
||||
<div
|
||||
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
||||
className={
|
||||
'bg-primary/10 mb-6 flex size-8 items-center-safe justify-center-safe rounded-full text-white'
|
||||
}
|
||||
>
|
||||
<HeartPulse className="size-4 fill-green-500" />
|
||||
</div>
|
||||
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
||||
<div className="bg-warning ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="px-2 text-black"
|
||||
onClick={() => handleSelect(variant.id)}
|
||||
>
|
||||
{variantAddingToCart === variant.id ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||
{variantAddingToCart === variant.id ? (
|
||||
<Loader2 className="size-4 animate-spin stroke-2" />
|
||||
) : (
|
||||
<ShoppingCart className="size-4 stroke-2" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex gap-2">
|
||||
<div className="flex flex-col items-start gap-2 flex-1">
|
||||
<div className="flex flex-1 flex-col items-start gap-2">
|
||||
<h5>
|
||||
{title}
|
||||
{description && (
|
||||
@@ -104,7 +106,7 @@ export default function OrderAnalysesCards({
|
||||
{' '}
|
||||
<InfoTooltip
|
||||
content={
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>{formattedPrice}</span>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
@@ -113,11 +115,7 @@ export default function OrderAnalysesCards({
|
||||
</>
|
||||
)}
|
||||
</h5>
|
||||
{subtitle && (
|
||||
<CardDescription>
|
||||
{subtitle}
|
||||
</CardDescription>
|
||||
)}
|
||||
{subtitle && <CardDescription>{subtitle}</CardDescription>}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 self-end text-sm">
|
||||
<span>{formattedPrice}</span>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||
import { StoreOrder } from '@medusajs/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { formatCurrency } from "@/packages/shared/src/utils"
|
||||
import { StoreOrder } from "@medusajs/types"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export default function CartTotals({ medusaOrder }: {
|
||||
medusaOrder: StoreOrder
|
||||
export default function CartTotals({
|
||||
medusaOrder,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
}) {
|
||||
const { i18n: { language } } = useTranslation()
|
||||
const {
|
||||
i18n: { language },
|
||||
} = useTranslation();
|
||||
const {
|
||||
currency_code,
|
||||
total,
|
||||
@@ -17,29 +23,39 @@ export default function CartTotals({ medusaOrder }: {
|
||||
tax_total,
|
||||
discount_total,
|
||||
gift_card_total,
|
||||
} = medusaOrder
|
||||
} = medusaOrder;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-y-2 txt-medium text-ui-fg-subtle ">
|
||||
<div className="txt-medium text-ui-fg-subtle flex flex-col gap-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex gap-x-1 items-center">
|
||||
<span className="flex items-center gap-x-1">
|
||||
<Trans i18nKey="cart:order.subtotal" />
|
||||
</span>
|
||||
<span data-testid="cart-subtotal" data-value={subtotal || 0}>
|
||||
{formatCurrency({ value: subtotal ?? 0, currencyCode: currency_code, locale: language })}
|
||||
{formatCurrency({
|
||||
value: subtotal ?? 0,
|
||||
currencyCode: currency_code,
|
||||
locale: language,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{!!discount_total && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span><Trans i18nKey="cart:order.promotionsTotal" /></span>
|
||||
<span>
|
||||
<Trans i18nKey="cart:order.promotionsTotal" />
|
||||
</span>
|
||||
<span
|
||||
className="text-ui-fg-interactive"
|
||||
data-testid="cart-discount"
|
||||
data-value={discount_total || 0}
|
||||
>
|
||||
-{" "}
|
||||
{formatCurrency({ value: discount_total ?? 0, currencyCode: currency_code, locale: language })}
|
||||
-{' '}
|
||||
{formatCurrency({
|
||||
value: discount_total ?? 0,
|
||||
currencyCode: currency_code,
|
||||
locale: language,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -53,30 +69,42 @@ export default function CartTotals({ medusaOrder }: {
|
||||
</div> */}
|
||||
{!!gift_card_total && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span><Trans i18nKey="cart:order.giftCard" /></span>
|
||||
<span>
|
||||
<Trans i18nKey="cart:order.giftCard" />
|
||||
</span>
|
||||
<span
|
||||
className="text-ui-fg-interactive"
|
||||
data-testid="cart-gift-card-amount"
|
||||
data-value={gift_card_total || 0}
|
||||
>
|
||||
-{" "}
|
||||
{formatCurrency({ value: gift_card_total ?? 0, currencyCode: currency_code, locale: language })}
|
||||
-{' '}
|
||||
{formatCurrency({
|
||||
value: gift_card_total ?? 0,
|
||||
currencyCode: currency_code,
|
||||
locale: language,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-px w-full border-b border-gray-200 my-4" />
|
||||
<div className="flex items-center justify-between text-ui-fg-base mb-2 txt-medium ">
|
||||
<span className="font-bold"><Trans i18nKey="cart:order.total" /></span>
|
||||
<div className="my-4 h-px w-full border-b border-gray-200" />
|
||||
<div className="text-ui-fg-base txt-medium mb-2 flex items-center justify-between">
|
||||
<span className="font-bold">
|
||||
<Trans i18nKey="cart:order.total" />
|
||||
</span>
|
||||
<span
|
||||
className="txt-xlarge-plus"
|
||||
data-testid="cart-total"
|
||||
data-value={total || 0}
|
||||
>
|
||||
{formatCurrency({ value: total ?? 0, currencyCode: currency_code, locale: language })}
|
||||
{formatCurrency({
|
||||
value: total ?? 0,
|
||||
currencyCode: currency_code,
|
||||
locale: language,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-px w-full border-b border-gray-200 mt-4" />
|
||||
<div className="mt-4 h-px w-full border-b border-gray-200" />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { formatDate } from 'date-fns';
|
||||
import type { AnalysisOrder } from "~/lib/types/analysis-order";
|
||||
|
||||
export default function OrderDetails({ order }: {
|
||||
order: AnalysisOrder
|
||||
}) {
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import type { AnalysisOrder } from '~/lib/types/analysis-order';
|
||||
|
||||
export default function OrderDetails({ order }: { order: AnalysisOrder }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div>
|
||||
<span className="font-bold">
|
||||
<Trans i18nKey="cart:orderConfirmed.orderNumber" />:{" "}
|
||||
</span>
|
||||
<span>
|
||||
{order.medusa_order_id}
|
||||
<Trans i18nKey="cart:orderConfirmed.orderNumber" />:{' '}
|
||||
</span>
|
||||
<span>{order.medusa_order_id}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-bold">
|
||||
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
|
||||
</span>
|
||||
<span>
|
||||
{formatDate(order.created_at, 'dd.MM.yyyy HH:mm')}
|
||||
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{' '}
|
||||
</span>
|
||||
<span>{formatDate(order.created_at, 'dd.MM.yyyy HH:mm')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { StoreCartLineItem, StoreOrderLineItem } from "@medusajs/types"
|
||||
import { TableCell, TableRow } from "@kit/ui/table"
|
||||
|
||||
import { StoreCartLineItem, StoreOrderLineItem } from '@medusajs/types';
|
||||
// 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"
|
||||
import LineItemPrice from '@modules/common/components/line-item-price';
|
||||
import LineItemUnitPrice from '@modules/common/components/line-item-unit-price';
|
||||
|
||||
export default function OrderItem({ item, currencyCode }: {
|
||||
item: StoreCartLineItem | StoreOrderLineItem
|
||||
currencyCode: string
|
||||
import { TableCell, TableRow } from '@kit/ui/table';
|
||||
|
||||
export default function OrderItem({
|
||||
item,
|
||||
currencyCode,
|
||||
}: {
|
||||
item: StoreCartLineItem | StoreOrderLineItem;
|
||||
currencyCode: string;
|
||||
}) {
|
||||
const partnerLocationName = item.metadata?.partner_location_name;
|
||||
return (
|
||||
@@ -18,22 +21,21 @@ export default function OrderItem({ item, currencyCode }: {
|
||||
</div>
|
||||
</TableCell> */}
|
||||
|
||||
<TableCell className="text-left px-6">
|
||||
<TableCell className="px-6 text-left">
|
||||
<span
|
||||
className="txt-medium-plus text-ui-fg-base"
|
||||
data-testid="product-name"
|
||||
>
|
||||
{item.product_title}{` ${partnerLocationName ? `(${partnerLocationName})` : ''}`}
|
||||
{item.product_title}
|
||||
{` ${partnerLocationName ? `(${partnerLocationName})` : ''}`}
|
||||
</span>
|
||||
{/* <LineItemOptions variant={item.variant} data-testid="product-variant" /> */}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-6">
|
||||
<span className="flex flex-col items-end h-full justify-center">
|
||||
<span className="flex gap-x-1 ">
|
||||
<span className="text-ui-fg-muted">
|
||||
{item.quantity}x{" "}
|
||||
</span>
|
||||
<span className="flex h-full flex-col items-end justify-center">
|
||||
<span className="flex gap-x-1">
|
||||
<span className="text-ui-fg-muted">{item.quantity}x </span>
|
||||
<LineItemUnitPrice
|
||||
item={item}
|
||||
style="tight"
|
||||
@@ -49,5 +51,5 @@ export default function OrderItem({ item, currencyCode }: {
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,44 @@
|
||||
import repeat from "@lib/util/repeat"
|
||||
import { StoreOrder } from "@medusajs/types"
|
||||
import { Table, TableBody } from "@kit/ui/table"
|
||||
import repeat from '@lib/util/repeat';
|
||||
import { StoreOrder } from '@medusajs/types';
|
||||
import SkeletonLineItem from '@modules/skeletons/components/skeleton-line-item';
|
||||
|
||||
import SkeletonLineItem from "@modules/skeletons/components/skeleton-line-item"
|
||||
import OrderItem from "./order-item"
|
||||
import { Heading } from "@kit/ui/heading"
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Table, TableBody } from '@kit/ui/table';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export default function OrderItems({ medusaOrder }: {
|
||||
medusaOrder: StoreOrder
|
||||
import OrderItem from './order-item';
|
||||
|
||||
export default function OrderItems({
|
||||
medusaOrder,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
}) {
|
||||
const items = medusaOrder.items
|
||||
const items = medusaOrder.items;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Heading level={5} className="flex flex-row text-3xl-regular">
|
||||
<Heading level={5} className="text-3xl-regular flex flex-row">
|
||||
<Trans i18nKey="cart:orderConfirmed.summary" />
|
||||
</Heading>
|
||||
<div className="flex flex-col">
|
||||
<Table className="rounded-lg border border-separate">
|
||||
<Table className="border-separate rounded-lg border">
|
||||
<TableBody data-testid="products-table">
|
||||
{items?.length
|
||||
? items
|
||||
.sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1)
|
||||
.map((item) => (
|
||||
<OrderItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
currencyCode={medusaOrder.currency_code}
|
||||
/>
|
||||
))
|
||||
.sort((a, b) =>
|
||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||
)
|
||||
.map((item) => (
|
||||
<OrderItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
currencyCode={medusaOrder.currency_code}
|
||||
/>
|
||||
))
|
||||
: repeat(5).map((i) => <SkeletonLineItem key={i} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
'use server';
|
||||
|
||||
import { createPageViewLog, PageViewAction } from "~/lib/services/audit/pageView.service";
|
||||
import { loadCurrentUserAccount } from "../../_lib/server/load-user-account";
|
||||
import {
|
||||
PageViewAction,
|
||||
createPageViewLog,
|
||||
} from '~/lib/services/audit/pageView.service';
|
||||
|
||||
export async function logAnalysisResultsNavigateAction(analysisOrderId: string) {
|
||||
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
||||
|
||||
export async function logAnalysisResultsNavigateAction(
|
||||
analysisOrderId: string,
|
||||
) {
|
||||
const { account } = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
|
||||
@@ -1,37 +1,57 @@
|
||||
import type { AnalysisOrder } from "~/lib/types/analysis-order";
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
import { StoreOrderLineItem } from "@medusajs/types";
|
||||
import OrderItemsTable from "./order-items-table";
|
||||
import Link from "next/link";
|
||||
import { Eye } from "lucide-react";
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function OrderBlock({ analysisOrder, itemsAnalysisPackage, itemsOther }: {
|
||||
analysisOrder: AnalysisOrder,
|
||||
itemsAnalysisPackage: StoreOrderLineItem[],
|
||||
itemsOther: StoreOrderLineItem[],
|
||||
import { StoreOrderLineItem } from '@medusajs/types';
|
||||
import { Eye } from 'lucide-react';
|
||||
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
|
||||
import type { AnalysisOrder } from '~/lib/types/analysis-order';
|
||||
|
||||
import OrderItemsTable from './order-items-table';
|
||||
|
||||
export default function OrderBlock({
|
||||
analysisOrder,
|
||||
itemsAnalysisPackage,
|
||||
itemsOther,
|
||||
}: {
|
||||
analysisOrder: AnalysisOrder;
|
||||
itemsAnalysisPackage: StoreOrderLineItem[];
|
||||
itemsOther: StoreOrderLineItem[];
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h4>
|
||||
<Trans i18nKey="analysis-results:orderTitle" values={{ orderNumber: analysisOrder.medusa_order_id }} />
|
||||
<Trans
|
||||
i18nKey="analysis-results:orderTitle"
|
||||
values={{ orderNumber: analysisOrder.medusa_order_id }}
|
||||
/>
|
||||
{` (${analysisOrder.id})`}
|
||||
</h4>
|
||||
<div className="flex gap-2">
|
||||
<h5>
|
||||
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
||||
</h5>
|
||||
<Link href={`/home/order/${analysisOrder.id}`} className="flex items-center justify-between text-small-regular">
|
||||
<button
|
||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
||||
>
|
||||
<Link
|
||||
href={`/home/order/${analysisOrder.id}`}
|
||||
className="text-small-regular flex items-center justify-between"
|
||||
>
|
||||
<button className="text-ui-fg-subtle hover:text-ui-fg-base flex cursor-pointer gap-x-1">
|
||||
<Eye />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<OrderItemsTable items={itemsAnalysisPackage} title="orders:table.analysisPackage" analysisOrder={analysisOrder} />
|
||||
<OrderItemsTable items={itemsOther} title="orders:table.otherOrders" analysisOrder={analysisOrder} />
|
||||
<OrderItemsTable
|
||||
items={itemsAnalysisPackage}
|
||||
title="orders:table.analysisPackage"
|
||||
analysisOrder={analysisOrder}
|
||||
/>
|
||||
<OrderItemsTable
|
||||
items={itemsOther}
|
||||
title="orders:table.otherOrders"
|
||||
analysisOrder={analysisOrder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function OrderItemsTable({
|
||||
)
|
||||
.map((orderItem) => (
|
||||
<TableRow className="w-full" key={orderItem.id}>
|
||||
<TableCell className="text-left w-[100%] px-6">
|
||||
<TableCell className="w-[100%] px-6 text-left">
|
||||
<p className="txt-medium-plus text-ui-fg-base">
|
||||
{orderItem.product_title}
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
|
||||
import { createAccountsApi } from '@kit/accounts/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { getProductCategories } from '@lib/data/categories';
|
||||
import { listProducts, listProductTypes } from '@lib/data/products';
|
||||
import { listProductTypes, listProducts } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
|
||||
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
|
||||
@@ -40,9 +40,9 @@ async function analysesLoader() {
|
||||
);
|
||||
const categoryProducts = category
|
||||
? await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, category_id: category.id, order: 'title' },
|
||||
})
|
||||
countryCode,
|
||||
queryParams: { limit: 100, category_id: category.id, order: 'title' },
|
||||
})
|
||||
: null;
|
||||
|
||||
const serviceCategories = productCategories.filter(
|
||||
@@ -52,21 +52,24 @@ async function analysesLoader() {
|
||||
return {
|
||||
analyses:
|
||||
categoryProducts?.response.products
|
||||
.filter(({ status, metadata }) => status === 'published' && !!metadata?.analysisIdOriginal)
|
||||
.map<OrderAnalysisCard>(
|
||||
({ title, description, subtitle, variants }) => {
|
||||
const variant = variants![0]!;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
variant: {
|
||||
id: variant.id,
|
||||
},
|
||||
price: variant.calculated_price?.calculated_amount ?? null,
|
||||
};
|
||||
},
|
||||
) ?? [],
|
||||
.filter(
|
||||
({ status, metadata }) =>
|
||||
status === 'published' && !!metadata?.analysisIdOriginal,
|
||||
)
|
||||
.map<OrderAnalysisCard>(
|
||||
({ title, description, subtitle, variants }) => {
|
||||
const variant = variants![0]!;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
variant: {
|
||||
id: variant.id,
|
||||
},
|
||||
price: variant.calculated_price?.calculated_amount ?? null,
|
||||
};
|
||||
},
|
||||
) ?? [],
|
||||
countryCode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { listProductTypes, listProducts } from "@lib/data/products";
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
import { listProductTypes, listProducts } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import type { StoreProduct } from '@medusajs/types';
|
||||
import { loadCurrentUserAccount } from './load-user-account';
|
||||
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import type { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||
|
||||
import PersonalCode from '~/lib/utils';
|
||||
|
||||
import { loadCurrentUserAccount } from './load-user-account';
|
||||
|
||||
async function countryCodesLoader() {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
|
||||
@@ -33,24 +36,25 @@ function userSpecificVariantLoader({
|
||||
throw new Error('Personal code not found');
|
||||
}
|
||||
|
||||
const { ageRange, gender: { value: gender } } = PersonalCode.parsePersonalCode(personalCode);
|
||||
const {
|
||||
ageRange,
|
||||
gender: { value: gender },
|
||||
} = PersonalCode.parsePersonalCode(personalCode);
|
||||
|
||||
return ({
|
||||
product,
|
||||
}: {
|
||||
product: StoreProduct;
|
||||
}) => {
|
||||
return ({ product }: { product: StoreProduct }) => {
|
||||
const variants = product.variants;
|
||||
if (!variants) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variant = variants.find((v) => v.options?.every((o) => [ageRange, gender].includes(o.value)));
|
||||
const variant = variants.find((v) =>
|
||||
v.options?.every((o) => [ageRange, gender].includes(o.value)),
|
||||
);
|
||||
if (!variant) {
|
||||
return null;
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function analysisPackageElementsLoader({
|
||||
@@ -60,30 +64,46 @@ async function analysisPackageElementsLoader({
|
||||
analysisPackagesWithVariant: AnalysisPackageWithVariant[];
|
||||
countryCode: string;
|
||||
}) {
|
||||
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(analysisPackagesWithVariant);
|
||||
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(
|
||||
analysisPackagesWithVariant,
|
||||
);
|
||||
if (analysisElementMedusaProductIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { response: { products } } = await listProducts({
|
||||
const {
|
||||
response: { products },
|
||||
} = await listProducts({
|
||||
countryCode,
|
||||
queryParams: {
|
||||
id: analysisElementMedusaProductIds,
|
||||
limit: 100,
|
||||
order: "title",
|
||||
order: 'title',
|
||||
},
|
||||
});
|
||||
|
||||
const standardPackage = analysisPackagesWithVariant.find(({ isStandard }) => isStandard);
|
||||
const standardPlusPackage = analysisPackagesWithVariant.find(({ isStandardPlus }) => isStandardPlus);
|
||||
const premiumPackage = analysisPackagesWithVariant.find(({ isPremium }) => isPremium);
|
||||
const standardPackage = analysisPackagesWithVariant.find(
|
||||
({ isStandard }) => isStandard,
|
||||
);
|
||||
const standardPlusPackage = analysisPackagesWithVariant.find(
|
||||
({ isStandardPlus }) => isStandardPlus,
|
||||
);
|
||||
const premiumPackage = analysisPackagesWithVariant.find(
|
||||
({ isPremium }) => isPremium,
|
||||
);
|
||||
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const standardPackageAnalyses = getAnalysisElementMedusaProductIds([standardPackage]);
|
||||
const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([standardPlusPackage]);
|
||||
const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([premiumPackage]);
|
||||
const standardPackageAnalyses = getAnalysisElementMedusaProductIds([
|
||||
standardPackage,
|
||||
]);
|
||||
const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([
|
||||
standardPlusPackage,
|
||||
]);
|
||||
const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([
|
||||
premiumPackage,
|
||||
]);
|
||||
|
||||
return products.map(({ id, title, description }) => ({
|
||||
id,
|
||||
@@ -103,18 +123,20 @@ async function analysisPackagesWithVariantLoader({
|
||||
countryCode: string;
|
||||
}) {
|
||||
const productTypes = await loadProductTypes();
|
||||
const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
const productType = productTypes.find(
|
||||
({ metadata }) => metadata?.handle === 'analysis-packages',
|
||||
);
|
||||
if (!productType) {
|
||||
return null;
|
||||
}
|
||||
const analysisPackagesResponse = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, "type_id[0]": productType.id },
|
||||
queryParams: { limit: 100, 'type_id[0]': productType.id },
|
||||
});
|
||||
|
||||
const getVariant = userSpecificVariantLoader({ account });
|
||||
const analysisPackagesWithVariant = analysisPackagesResponse.response.products
|
||||
.reduce((acc, product) => {
|
||||
const analysisPackagesWithVariant =
|
||||
analysisPackagesResponse.response.products.reduce((acc, product) => {
|
||||
const variant = getVariant({ product });
|
||||
if (!variant) {
|
||||
return acc;
|
||||
@@ -124,14 +146,17 @@ async function analysisPackagesWithVariantLoader({
|
||||
{
|
||||
variant,
|
||||
variantId: variant.id,
|
||||
nrOfAnalyses: getAnalysisElementMedusaProductIds([{ ...product, variant }]).length,
|
||||
nrOfAnalyses: getAnalysisElementMedusaProductIds([
|
||||
{ ...product, variant },
|
||||
]).length,
|
||||
price: variant.calculated_price?.calculated_amount ?? 0,
|
||||
title: product.title,
|
||||
subtitle: product.subtitle,
|
||||
description: product.description,
|
||||
metadata: product.metadata,
|
||||
isStandard: product.metadata?.analysisPackageTier === 'standard',
|
||||
isStandardPlus: product.metadata?.analysisPackageTier === 'standard-plus',
|
||||
isStandardPlus:
|
||||
product.metadata?.analysisPackageTier === 'standard-plus',
|
||||
isPremium: product.metadata?.analysisPackageTier === 'premium',
|
||||
},
|
||||
];
|
||||
@@ -149,13 +174,23 @@ async function analysisPackagesLoader() {
|
||||
const countryCodes = await loadCountryCodes();
|
||||
const countryCode = countryCodes[0]!;
|
||||
|
||||
const analysisPackagesWithVariant = await analysisPackagesWithVariantLoader({ account, countryCode });
|
||||
const analysisPackagesWithVariant = await analysisPackagesWithVariantLoader({
|
||||
account,
|
||||
countryCode,
|
||||
});
|
||||
if (!analysisPackagesWithVariant) {
|
||||
return { analysisPackageElements: [], analysisPackages: [], countryCode };
|
||||
}
|
||||
|
||||
const analysisPackageElements = await analysisPackageElementsLoader({ analysisPackagesWithVariant, countryCode });
|
||||
const analysisPackageElements = await analysisPackageElementsLoader({
|
||||
analysisPackagesWithVariant,
|
||||
countryCode,
|
||||
});
|
||||
|
||||
return { analysisPackageElements, analysisPackages: analysisPackagesWithVariant, countryCode };
|
||||
return {
|
||||
analysisPackageElements,
|
||||
analysisPackages: analysisPackagesWithVariant,
|
||||
countryCode,
|
||||
};
|
||||
}
|
||||
export const loadAnalysisPackages = cache(analysisPackagesLoader);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
|
||||
|
||||
import { createAccountsApi } from '@kit/accounts/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
|
||||
|
||||
export type UserAccount = Awaited<ReturnType<typeof loadUserAccount>>;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
'use server';
|
||||
|
||||
import { retrieveCart, updateCart, updateLineItem } from "@lib/data/cart";
|
||||
import { retrieveCart, updateCart, updateLineItem } from '@lib/data/cart';
|
||||
|
||||
export const updateCartPartnerLocation = async ({
|
||||
cartId,
|
||||
@@ -15,7 +15,7 @@ export const updateCartPartnerLocation = async ({
|
||||
}) => {
|
||||
const cart = await retrieveCart(cartId);
|
||||
if (!cart) {
|
||||
throw new Error("Cart not found");
|
||||
throw new Error('Cart not found');
|
||||
}
|
||||
|
||||
for (const lineItemId of lineIds) {
|
||||
@@ -35,4 +35,4 @@ export const updateCartPartnerLocation = async ({
|
||||
partner_location_id: partnerLocationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import dynamic from 'next/dynamic';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { PlanPicker } from '@kit/billing-gateway/components';
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
import { useAppEvents } from '@kit/shared/events';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -19,8 +20,6 @@ import {
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
|
||||
import { createPersonalAccountCheckoutSession } from '../_lib/server/server-actions';
|
||||
|
||||
const EmbeddedCheckout = dynamic(
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { featureFlagsConfig } from '@kit/shared/config';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema';
|
||||
import { createUserBillingService } from './user-billing.service';
|
||||
import { featureFlagsConfig } from '@kit/shared/config';
|
||||
|
||||
/**
|
||||
* @name enabled
|
||||
|
||||
@@ -8,12 +8,10 @@ import { z } from 'zod';
|
||||
import { createAccountsApi } from '@kit/accounts/api';
|
||||
import { getProductPlanPair } from '@kit/billing';
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
|
||||
import { appConfig, billingConfig } from '@kit/shared/config';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
|
||||
import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { featureFlagsConfig } from '@kit/shared/config';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { featureFlagsConfig } from '@kit/shared/config';
|
||||
|
||||
function UserBillingLayout(props: React.PropsWithChildren) {
|
||||
const isEnabled = featureFlagsConfig.enablePersonalAccountBilling;
|
||||
|
||||
@@ -3,17 +3,17 @@ import {
|
||||
CurrentLifetimeOrderCard,
|
||||
CurrentSubscriptionCard,
|
||||
} from '@kit/billing-gateway/components';
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
|
||||
// local imports
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
|
||||
import { HomeLayoutPageHeader } from '../_components/home-page-header';
|
||||
import { createPersonalAccountBillingPortalSession } from '../billing/_lib/server/server-actions';
|
||||
import { PersonalAccountCheckoutForm } from './_components/personal-account-checkout-form';
|
||||
|
||||
@@ -4,11 +4,12 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardDescription, CardTitle } from '@kit/ui/card';
|
||||
import { Form } from '@kit/ui/form';
|
||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Switch } from '@kit/ui/switch';
|
||||
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
accountPreferencesSchema,
|
||||
} from '../_lib/account-preferences.schema';
|
||||
import { updatePersonalAccountPreferencesAction } from '../_lib/server/actions';
|
||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
||||
|
||||
export default function AccountPreferencesForm({
|
||||
account,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
@@ -129,11 +129,7 @@ export default function AccountSettingsForm({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="cm"
|
||||
type="number"
|
||||
{...field}
|
||||
/>
|
||||
<Input placeholder="cm" type="number" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -152,11 +148,7 @@ export default function AccountSettingsForm({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="kg"
|
||||
type="number"
|
||||
{...field}
|
||||
/>
|
||||
<Input placeholder="kg" type="number" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Cross, Menu, Shield, ShoppingCart } from 'lucide-react';
|
||||
|
||||
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { ApplicationRoleEnum } from '@kit/accounts/types/accounts';
|
||||
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
|
||||
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import {
|
||||
@@ -22,8 +24,6 @@ import {
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
|
||||
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
|
||||
import { UserWorkspace } from '../../_lib/server/load-user-workspace';
|
||||
import { routes } from './settings-sidebar';
|
||||
|
||||
@@ -148,5 +148,3 @@ export function SettingsMobileNavigation(props: {
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator } from "@kit/ui/separator";
|
||||
import { Trans } from "@kit/ui/trans";
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export default function SettingsSectionHeader({
|
||||
titleKey,
|
||||
|
||||
@@ -11,11 +11,11 @@ import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SettingsSidebar } from './_components/settings-sidebar';
|
||||
// home imports
|
||||
import { HomeMenuNavigation } from '../_components/home-menu-navigation';
|
||||
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
|
||||
import { SettingsMobileNavigation } from './_components/settings-navigation';
|
||||
import { SettingsSidebar } from './_components/settings-sidebar';
|
||||
|
||||
function UserSettingsLayout({ children }: React.PropsWithChildren) {
|
||||
return <HeaderLayout>{children}</HeaderLayout>;
|
||||
@@ -27,7 +27,6 @@ function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
const cart = use(retrieveCart());
|
||||
|
||||
|
||||
return (
|
||||
<UserWorkspaceContextProvider value={workspace}>
|
||||
<Page style={'header'}>
|
||||
@@ -44,7 +43,7 @@ function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
<PageNavigation>
|
||||
<SettingsSidebar />
|
||||
</PageNavigation>
|
||||
<div className="md:mt-28 min-w-full min-h-full">{children}</div>
|
||||
<div className="min-h-full min-w-full md:mt-28">{children}</div>
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
</Page>
|
||||
|
||||
@@ -5,11 +5,9 @@ import { useContext } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||
import { SidebarContext } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { pathsConfig, featureFlagsConfig } from '@kit/shared/config';
|
||||
|
||||
|
||||
const features = {
|
||||
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
|
||||
import { Home, Menu } from 'lucide-react';
|
||||
|
||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
|
||||
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
|
||||
import {
|
||||
featureFlagsConfig,
|
||||
getTeamAccountSidebarConfig,
|
||||
@@ -93,8 +93,6 @@ export const TeamAccountLayoutMobileNavigation = (
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
function TeamAccountsModal(props: {
|
||||
accounts: Accounts;
|
||||
userId: string;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NotificationsPopover } from '@kit/notifications/components';
|
||||
|
||||
import { featureFlagsConfig } from '@kit/shared/config';
|
||||
|
||||
|
||||
export function TeamAccountNotifications(params: {
|
||||
userId: string;
|
||||
accountId: string;
|
||||
|
||||
@@ -4,11 +4,10 @@ import { cache } from 'react';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createTeamAccountsApi } from '@kit/team-accounts/api';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
|
||||
export type TeamAccountWorkspace = Awaited<
|
||||
@@ -29,7 +28,7 @@ export const loadTeamWorkspace = cache(workspaceLoader);
|
||||
async function workspaceLoader(accountSlug: string) {
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createTeamAccountsApi(client);
|
||||
const user = await requireUserInServerComponent();
|
||||
const user = await requireUserInServerComponent();
|
||||
const workspace = await api.getAccountWorkspace(accountSlug, user.id);
|
||||
// we cannot find any record for the selected account
|
||||
// so we redirect the user to the home page
|
||||
@@ -39,7 +38,9 @@ async function workspaceLoader(accountSlug: string) {
|
||||
|
||||
return {
|
||||
...workspace.data,
|
||||
accounts: workspace.data.accounts.map(({ user_accounts }) => ({...user_accounts})),
|
||||
accounts: workspace.data.accounts.map(({ user_accounts }) => ({
|
||||
...user_accounts,
|
||||
})),
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dynamic from 'next/dynamic';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
import { PlanPicker } from '@kit/billing-gateway/components';
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
import { useAppEvents } from '@kit/shared/events';
|
||||
import {
|
||||
Card,
|
||||
@@ -16,8 +17,6 @@ import {
|
||||
} from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
|
||||
import { createTeamAccountCheckoutSession } from '../_lib/server/server-actions';
|
||||
|
||||
const EmbeddedCheckout = dynamic(
|
||||
|
||||
@@ -6,7 +6,6 @@ import { PageBody } from '@kit/ui/page';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
import HealthBenefitForm from './_components/health-benefit-form';
|
||||
|
||||
interface TeamAccountBillingPageProps {
|
||||
|
||||
@@ -5,10 +5,10 @@ import { BillingSessionStatus } from '@kit/billing-gateway/components';
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
|
||||
import { EmbeddedCheckoutForm } from '../_components/embedded-checkout-form';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
interface SessionPageProps {
|
||||
searchParams: Promise<{
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
// local imports
|
||||
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
|
||||
import { loadMembersPageData } from './_lib/server/members-page.loader';
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/ser
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { createUserAnalysesApi } from '@kit/user-analyses/api';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
Reference in New Issue
Block a user