prettier fix

This commit is contained in:
Danel Kungla
2025-09-19 17:22:36 +03:00
parent efa94b3322
commit 0c2cfe6d18
509 changed files with 17988 additions and 9920 deletions

View File

@@ -2,7 +2,6 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();

View File

@@ -2,7 +2,6 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();

View File

@@ -7,15 +7,14 @@ import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace
import { useQuery } from '@tanstack/react-query';
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
import { useAuthConfig } from '@kit/shared/hooks';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
import { useAuthConfig } from '@kit/shared/hooks';
const ModeToggle = dynamic(() =>
import('@kit/ui/mode-toggle').then((mod) => ({
default: mod.ModeToggle,

View File

@@ -1,8 +1,7 @@
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { Header } from '@kit/ui/marketing';
import { AppLogo } from '@kit/shared/components/app-logo';
import { Header } from '@kit/ui/marketing';
import { SiteHeaderAccountSection } from './site-header-account-section';

View File

@@ -8,7 +8,6 @@ import { createCmsClient } from '@kit/cms';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Post } from '../../blog/_components/post';
interface BlogPageProps {

View File

@@ -11,14 +11,16 @@ const contactEmail = z
.string({
error:
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
}).describe(`The email where you want to receive the contact form submissions.`)
})
.describe(`The email where you want to receive the contact form submissions.`)
.parse(process.env.CONTACT_EMAIL);
const emailFrom = z
.string({
error:
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
}).describe(`The email sending address.`)
})
.describe(`The email sending address.`)
.parse(process.env.EMAIL_SENDER);
export const sendContactEmail = enhanceAction(

View File

@@ -6,7 +6,6 @@ import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();

View File

@@ -9,7 +9,6 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();

View File

@@ -2,9 +2,8 @@ import { use } from 'react';
import { SiteFooter } from '~/(marketing)/_components/site-footer';
import { SiteHeader } from '~/(marketing)/_components/site-header';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadCurrentUserAccounts } from '~/home/(user)/_lib/server/load-accounts';
import { withI18n } from '~/lib/i18n/with-i18n';
function SiteLayout(props: React.PropsWithChildren) {
const accounts = use(loadCurrentUserAccounts());

View File

@@ -1,9 +1,9 @@
import Link from 'next/link';
import { ArrowRightIcon } from 'lucide-react';
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
import { pathsConfig } from '@kit/shared/config';
import { ArrowRightIcon } from 'lucide-react';
import { CtaButton, Hero } from '@kit/ui/marketing';
import { Trans } from '@kit/ui/trans';

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
import CompanyOfferForm from './_components/company-offer-form';
function CompanyOffer() {

View File

@@ -1,9 +1,8 @@
import { withI18n } from '~/lib/i18n/with-i18n';
function SiteLayout(props: React.PropsWithChildren) {
return (
<div className={'flex min-h-[100vh] flex-col justify-center items-center'}>
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
{props.children}
</div>
);

View File

@@ -6,6 +6,8 @@ import { usePathname } from 'next/navigation';
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { LayoutDashboard, Users } from 'lucide-react';
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import {
Sidebar,
SidebarContent,
@@ -19,9 +21,6 @@ import {
useSidebar,
} from '@kit/ui/shadcn-sidebar';
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
export function AdminSidebar({
accounts,
}: {

View File

@@ -2,6 +2,7 @@ import { cache } from 'react';
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { getAccount } from '~/lib/services/account.service';
interface Params {

View File

@@ -1,11 +1,10 @@
import { getPlanTypesMap } from '@kit/billing';
import { getBillingEventHandlerService } from '@kit/billing-gateway';
import { enhanceRouteHandler } from '@kit/next/routes';
import { billingConfig } from '@kit/shared/config';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { billingConfig } from '@kit/shared/config';
/**
* @description Handle the webhooks from Stripe related to checkouts
*/

View File

@@ -2,7 +2,11 @@ import { config } from 'dotenv';
export default function loadEnv() {
config({ path: `.env` });
if (['local', 'test', 'development', 'production'].includes(process.env.NODE_ENV!)) {
if (
['local', 'test', 'development', 'production'].includes(
process.env.NODE_ENV!,
)
) {
config({ path: `.env.${process.env.NODE_ENV}` });
}
}

View File

@@ -1,9 +1,13 @@
import Medusa from "@medusajs/js-sdk"
import type { AdminProductCategory } from "@medusajs/types";
import { listProductTypes } from "@lib/data/products";
import { getAnalysisElements } from "~/lib/services/analysis-element.service";
import { getAnalysisGroups } from "~/lib/services/analysis-group.service";
import { createMedusaSyncFailEntry, createMedusaSyncSuccessEntry } from "~/lib/services/analyses.service";
import { listProductTypes } from '@lib/data/products';
import Medusa from '@medusajs/js-sdk';
import type { AdminProductCategory } from '@medusajs/types';
import {
createMedusaSyncFailEntry,
createMedusaSyncSuccessEntry,
} from '~/lib/services/analyses.service';
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
import { getAnalysisGroups } from '~/lib/services/analysis-group.service';
const SYNLAB_SERVICES_CATEGORY_HANDLE = 'synlab-services';
const SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE = 'synlab-analysis';
@@ -12,7 +16,8 @@ const BASE_ANALYSIS_PRODUCT_HANDLE = 'analysis-base';
const getAdminSdk = () => {
const medusaBackendUrl = process.env.MEDUSA_BACKEND_PUBLIC_URL!;
const medusaPublishableApiKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!;
const medusaPublishableApiKey =
process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!;
const key = process.env.MEDUSA_SECRET_API_KEY!;
if (!medusaBackendUrl || !medusaPublishableApiKey) {
@@ -23,15 +28,14 @@ const getAdminSdk = () => {
debug: process.env.NODE_ENV === 'development',
apiKey: key,
});
}
};
async function createProductCategories({
medusa,
}: {
medusa: Medusa;
}) {
const { product_categories: existingProductCategories } = await medusa.admin.productCategory.list();
const parentCategory = existingProductCategories.find(({ handle }) => handle === SYNLAB_SERVICES_CATEGORY_HANDLE);
async function createProductCategories({ medusa }: { medusa: Medusa }) {
const { product_categories: existingProductCategories } =
await medusa.admin.productCategory.list();
const parentCategory = existingProductCategories.find(
({ handle }) => handle === SYNLAB_SERVICES_CATEGORY_HANDLE,
);
if (!parentCategory) {
throw new Error('Parent category not found');
@@ -46,8 +50,12 @@ async function createProductCategories({
for (const analysisGroup of analysisGroups) {
console.info(`Processing analysis group '${analysisGroup.name}'`);
const isExisting = existingProductCategories.find(({ name }) => name === analysisGroup.name);
const isNewlyCreated = createdCategories.find(({ name }) => name === analysisGroup.name);
const isExisting = existingProductCategories.find(
({ name }) => name === analysisGroup.name,
);
const isNewlyCreated = createdCategories.find(
({ name }) => name === analysisGroup.name,
);
if (isExisting || isNewlyCreated) {
console.info(`Analysis group '${analysisGroup.name}' already exists`);
continue;
@@ -63,18 +71,19 @@ async function createProductCategories({
analysisGroupId: analysisGroup.id,
},
});
console.info(`Successfully created category, id=${createResponse.product_category.id}`);
console.info(
`Successfully created category, id=${createResponse.product_category.id}`,
);
createdCategories.push(createResponse.product_category);
}
}
async function getChildProductCategories({
medusa,
}: {
medusa: Medusa;
}) {
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
const childCategories = allCategories.filter(({ parent_category_id }) => parent_category_id !== null);
async function getChildProductCategories({ medusa }: { medusa: Medusa }) {
const { product_categories: allCategories } =
await medusa.admin.productCategory.list();
const childCategories = allCategories.filter(
({ parent_category_id }) => parent_category_id !== null,
);
return childCategories;
}
@@ -93,34 +102,34 @@ async function deleteProductCategories({
/**
* In case a reset is needed
*/
async function deleteProducts({
medusa,
}: {
medusa: Medusa;
}) {
async function deleteProducts({ medusa }: { medusa: Medusa }) {
const { products: existingProducts } = await medusa.admin.product.list({
fields: 'id,collection_id',
limit: 1000,
});
await Promise.all(existingProducts.filter((a) => !a.collection_id).map(({ id }) => medusa.admin.product.delete(id)));
await Promise.all(
existingProducts
.filter((a) => !a.collection_id)
.map(({ id }) => medusa.admin.product.delete(id)),
);
}
async function getAnalysisPackagesType() {
const { productTypes } = await listProductTypes();
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE);
const analysisPackagesType = productTypes.find(
({ metadata }) => metadata?.handle === SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE,
);
if (!analysisPackagesType) {
throw new Error('Synlab analysis packages type not found');
}
return analysisPackagesType;
}
async function getProductDefaultFields({
medusa,
}: {
medusa: Medusa;
}) {
const baseProductsResponse = await medusa.admin.product.list({ handle: BASE_ANALYSIS_PRODUCT_HANDLE })
async function getProductDefaultFields({ medusa }: { medusa: Medusa }) {
const baseProductsResponse = await medusa.admin.product.list({
handle: BASE_ANALYSIS_PRODUCT_HANDLE,
});
const baseProduct = baseProductsResponse.products[0];
if (!baseProduct) {
throw new Error('Base product not found');
@@ -142,25 +151,18 @@ async function getProductDefaultFields({
defaultSalesChannels,
defaultProductOption,
defaultProductVariant,
}
};
}
async function createProducts({
medusa,
}: {
medusa: Medusa;
}) {
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
async function createProducts({ medusa }: { medusa: Medusa }) {
const { product_categories: allCategories } =
await medusa.admin.productCategory.list();
const [
{ products: existingProducts },
analysisElements,
analysisPackagesType,
{
defaultSalesChannels,
defaultProductOption,
defaultProductVariant,
}
{ defaultSalesChannels, defaultProductOption, defaultProductVariant },
] = await Promise.all([
medusa.admin.product.list({
category_id: allCategories.map(({ id }) => id),
@@ -168,13 +170,17 @@ async function createProducts({
getAnalysisElements({ getAll: true }),
getAnalysisPackagesType(),
getProductDefaultFields({ medusa }),
])
]);
for (const analysisElement of analysisElements) {
const { analysis_id_original: originalId } = analysisElement;
const isExisting = existingProducts.find(({ metadata }) => metadata?.analysisIdOriginal === originalId);
const isExisting = existingProducts.find(
({ metadata }) => metadata?.analysisIdOriginal === originalId,
);
if (isExisting) {
console.info(`Analysis element '${analysisElement.analysis_name_lab}' already exists`);
console.info(
`Analysis element '${analysisElement.analysis_name_lab}' already exists`,
);
continue;
}
const { analysis_name_lab: name } = analysisElement;
@@ -183,7 +189,10 @@ async function createProducts({
continue;
}
const category = allCategories.find(({ metadata }) => metadata?.analysisGroupId === analysisElement.parent_analysis_group_id);
const category = allCategories.find(
({ metadata }) =>
metadata?.analysisGroupId === analysisElement.parent_analysis_group_id,
);
if (!category) {
console.error(`Category not found for analysis element '${name}'`);
continue;

View File

@@ -1,16 +1,32 @@
import axios from 'axios';
import { XMLParser } from 'fast-xml-parser';
import fs from 'fs';
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types';
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry, getAnalyses } from '~/lib/services/analyses.service';
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
import { AnalysisElement, createAnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
import { createCodes } from '~/lib/services/codes.service';
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
import type { ICode } from '~/lib/types/code';
import { toArray } from '@kit/shared/utils';
import {
createAnalysis,
createNoDataReceivedEntry,
createNoNewDataReceivedEntry,
createSyncFailEntry,
createSyncSuccessEntry,
getAnalyses,
} from '~/lib/services/analyses.service';
import {
AnalysisElement,
createAnalysisElement,
getAnalysisElements,
} from '~/lib/services/analysis-element.service';
import {
createAnalysisGroup,
getAnalysisGroups,
} from '~/lib/services/analysis-group.service';
import { createCodes } from '~/lib/services/codes.service';
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types';
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
import type { ICode } from '~/lib/types/code';
const WRITE_XML_TO_FILE = false as boolean;
export default async function syncAnalysisGroups() {
@@ -52,7 +68,8 @@ export default async function syncAnalysisGroups() {
}
const parser = new XMLParser({ ignoreAttributes: false });
const parsed: IMedipostPublicMessageDataParsed = parser.parse(publicMessageData);
const parsed: IMedipostPublicMessageDataParsed =
parser.parse(publicMessageData);
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
throw new Error(
@@ -77,12 +94,18 @@ export default async function syncAnalysisGroups() {
const codes: ICode[] = [];
for (const analysisGroup of analysisGroups) {
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
const existingAnalysisGroup = existingAnalysisGroups?.find(
({ original_id }) => original_id === analysisGroup.UuringuGruppId,
);
let groupExistingAnalysisElements: AnalysisElement[] = [];
let analysisGroupId: number;
if (existingAnalysisGroup) {
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`);
groupExistingAnalysisElements = await getAnalysisElements({ analysisGroupId: existingAnalysisGroup.id });
console.info(
`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`,
);
groupExistingAnalysisElements = await getAnalysisElements({
analysisGroupId: existingAnalysisGroup.id,
});
analysisGroupId = existingAnalysisGroup.id;
} else {
analysisGroupId = await createAnalysisGroup({
@@ -109,10 +132,14 @@ export default async function syncAnalysisGroups() {
for (const item of analysisGroupItems) {
const analysisElement = item.UuringuElement!;
const isExistingAnalysisElement = groupExistingAnalysisElements
.find(({ analysis_id_original }) => analysis_id_original === analysisElement.UuringId);
const isExistingAnalysisElement = groupExistingAnalysisElements.find(
({ analysis_id_original }) =>
analysis_id_original === analysisElement.UuringId,
);
if (isExistingAnalysisElement) {
console.info(`Analysis element '${analysisElement.UuringNimi}' already exists`);
console.info(
`Analysis element '${analysisElement.UuringNimi}' already exists`,
);
continue;
}
@@ -149,16 +176,24 @@ export default async function syncAnalysisGroups() {
const analyses = analysisElement.UuringuElement;
if (analyses?.length) {
const existingAnalyses = await getAnalyses({ originalIds: analyses.map(({ UuringId }) => UuringId) });
const existingAnalyses = await getAnalyses({
originalIds: analyses.map(({ UuringId }) => UuringId),
});
for (const analysis of analyses) {
const isExistingAnalysis = existingAnalyses.find(({ analysis_id_original }) => analysis_id_original === analysis.UuringId);
const isExistingAnalysis = existingAnalyses.find(
({ analysis_id_original }) =>
analysis_id_original === analysis.UuringId,
);
if (isExistingAnalysis) {
console.info(`Analysis '${analysis.UuringNimi}' already exists`);
continue;
}
const insertedAnalysisId = await createAnalysis(analysis, analysisGroupId);
const insertedAnalysisId = await createAnalysis(
analysis,
analysisGroupId,
);
if (analysis.Kood) {
const analysisCodes = toArray(analysis.Kood);
@@ -187,11 +222,17 @@ export default async function syncAnalysisGroups() {
await createSyncSuccessEntry();
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
await createSyncFailEntry(JSON.stringify({
await createSyncFailEntry(
JSON.stringify(
{
message: errorMessage,
stack: e instanceof Error ? e.stack : undefined,
name: e instanceof Error ? e.name : 'Unknown',
}, null, 2));
},
null,
2,
),
);
console.error('Sync failed:', e);
throw new Error(
`Failed to sync public message data, error: ${errorMessage}`,

View File

@@ -1,4 +1,4 @@
import { readPrivateMessageResponse } from "~/lib/services/medipost/medipostPrivateMessage.service";
import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service';
type ProcessedMessage = {
messageId: string;
@@ -15,9 +15,9 @@ type GroupedResults = {
};
export default async function syncAnalysisResults() {
console.info("Syncing analysis results");
console.info('Syncing analysis results');
let processedMessages: ProcessedMessage[] = [];
const processedMessages: ProcessedMessage[] = [];
const excludedMessageIds: string[] = [];
while (true) {
const result = await readPrivateMessageResponse({ excludedMessageIds });
@@ -26,7 +26,7 @@ export default async function syncAnalysisResults() {
}
if (!result.messageId) {
console.info("No more messages to process");
console.info('No more messages to process');
break;
}
@@ -59,5 +59,7 @@ export default async function syncAnalysisResults() {
}
return acc;
}, {} as GroupedResults);
console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`);
console.info(
`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`,
);
}

View File

@@ -1,4 +1,4 @@
import { NextRequest } from "next/server";
import { NextRequest } from 'next/server';
export default function validateApiKey(request: NextRequest) {
const envApiKey = process.env.JOBS_API_TOKEN;

View File

@@ -1,10 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service";
import { retrieveOrder } from "@lib/data/orders";
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
import { NextRequest, NextResponse } from 'next/server';
import { retrieveOrder } from '@lib/data/orders';
import { getMedipostDispatchTries } from '~/lib/services/audit.service';
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
import loadEnv from '../handler/load-env';
import validateApiKey from '../handler/validate-api-key';
export const POST = async (request: NextRequest) => {
loadEnv();
@@ -19,23 +22,34 @@ export const POST = async (request: NextRequest) => {
const tries = await getMedipostDispatchTries(medusaOrderId);
if (tries >= 3) {
return NextResponse.json({
return NextResponse.json(
{
message: 'Order has been retried too many times',
}, { status: 400 });
},
{ status: 400 },
);
}
try {
const medusaOrder = await retrieveOrder(medusaOrderId);
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
const orderedAnalysisElements = await getOrderedAnalysisIds({
medusaOrder,
});
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
console.info("Successfully sent order to medipost");
return NextResponse.json({
console.info('Successfully sent order to medipost');
return NextResponse.json(
{
message: 'Successfully sent order to medipost',
}, { status: 200 });
},
{ status: 200 },
);
} catch (e) {
console.error("Error sending order to medipost", e);
return NextResponse.json({
console.error('Error sending order to medipost', e);
return NextResponse.json(
{
message: 'Failed to send order to medipost',
}, { status: 500 });
},
{ status: 500 },
);
}
};

View File

@@ -4,6 +4,7 @@ import {
NotificationAction,
createNotificationLog,
} from '~/lib/services/audit/notificationEntries.service';
import loadEnv from '../handler/load-env';
import sendOpenJobsEmails from '../handler/send-open-jobs-emails';
import validateApiKey from '../handler/validate-api-key';

View File

@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import syncAnalysisGroupsStore from "../handler/sync-analysis-groups-store";
import { NextRequest, NextResponse } from 'next/server';
import loadEnv from '../handler/load-env';
import syncAnalysisGroupsStore from '../handler/sync-analysis-groups-store';
import validateApiKey from '../handler/validate-api-key';
export const POST = async (request: NextRequest) => {
loadEnv();
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
try {
await syncAnalysisGroupsStore();
console.info("Successfully synced analysis groups store");
return NextResponse.json({
console.info('Successfully synced analysis groups store');
return NextResponse.json(
{
message: 'Successfully synced analysis groups store',
}, { status: 200 });
},
{ status: 200 },
);
} catch (e) {
console.error("Error syncing analysis groups store", e);
return NextResponse.json({
console.error('Error syncing analysis groups store', e);
return NextResponse.json(
{
message: 'Failed to sync analysis groups store',
}, { status: 500 });
},
{ status: 500 },
);
}
};

View File

@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import syncAnalysisGroups from "../handler/sync-analysis-groups";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import { NextRequest, NextResponse } from 'next/server';
import loadEnv from '../handler/load-env';
import syncAnalysisGroups from '../handler/sync-analysis-groups';
import validateApiKey from '../handler/validate-api-key';
export const POST = async (request: NextRequest) => {
loadEnv();
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
try {
await syncAnalysisGroups();
console.info("Successfully synced analysis groups");
return NextResponse.json({
console.info('Successfully synced analysis groups');
return NextResponse.json(
{
message: 'Successfully synced analysis groups',
}, { status: 200 });
},
{ status: 200 },
);
} catch (e) {
console.error("Error syncing analysis groups", e);
return NextResponse.json({
console.error('Error syncing analysis groups', e);
return NextResponse.json(
{
message: 'Failed to sync analysis groups',
}, { status: 500 });
},
{ status: 500 },
);
}
};

View File

@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import syncAnalysisResults from "../handler/sync-analysis-results";
import { NextRequest, NextResponse } from 'next/server';
import loadEnv from '../handler/load-env';
import syncAnalysisResults from '../handler/sync-analysis-results';
import validateApiKey from '../handler/validate-api-key';
export const POST = async (request: NextRequest) => {
loadEnv();
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
try {
await syncAnalysisResults();
console.info("Successfully synced analysis results");
return NextResponse.json({
console.info('Successfully synced analysis results');
return NextResponse.json(
{
message: 'Successfully synced analysis results',
}, { status: 200 });
},
{ status: 200 },
);
} catch (e) {
console.error("Error syncing analysis results", e);
return NextResponse.json({
console.error('Error syncing analysis results', e);
return NextResponse.json(
{
message: 'Failed to sync analysis results',
}, { status: 500 });
},
{ status: 500 },
);
}
};

View File

@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import syncConnectedOnline from "../handler/sync-connected-online";
import { NextRequest, NextResponse } from 'next/server';
import loadEnv from '../handler/load-env';
import syncConnectedOnline from '../handler/sync-connected-online';
import validateApiKey from '../handler/validate-api-key';
export const POST = async (request: NextRequest) => {
loadEnv();
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
try {
await syncConnectedOnline();
console.info("Successfully synced connected-online");
return NextResponse.json({
console.info('Successfully synced connected-online');
return NextResponse.json(
{
message: 'Successfully synced connected-online',
}, { status: 200 });
},
{ status: 200 },
);
} catch (e) {
console.error("Error syncing connected-online", e);
return NextResponse.json({
console.error('Error syncing connected-online', e);
return NextResponse.json(
{
message: 'Failed to sync connected-online',
}, { status: 500 });
},
{ status: 500 },
);
}
};

View File

@@ -1,11 +1,17 @@
import { NextRequest, NextResponse } from "next/server";
import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import { NextRequest, NextResponse } from 'next/server';
import { retrieveOrder } from '@lib/data';
import { getAccountAdmin } from '~/lib/services/account.service';
import {
composeOrderTestResponseXML,
sendPrivateMessageTestResponse,
} from '~/lib/services/medipost/medipostTest.service';
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
import { getAnalysisOrdersAdmin } from '~/lib/services/order.service';
import loadEnv from '../handler/load-env';
import validateApiKey from '../handler/validate-api-key';
export async function POST(request: NextRequest) {
loadEnv();
@@ -16,17 +22,27 @@ export async function POST(request: NextRequest) {
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
}
const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'PROCESSING' });
const analysisOrders = await getAnalysisOrdersAdmin({
orderStatus: 'PROCESSING',
});
console.error(`Sending test responses for ${analysisOrders.length} analysis orders`);
console.error(
`Sending test responses for ${analysisOrders.length} analysis orders`,
);
for (const medreportOrder of analysisOrders) {
const medusaOrderId = medreportOrder.medusa_order_id;
const medusaOrder = await retrieveOrder(medusaOrderId)
const medusaOrder = await retrieveOrder(medusaOrderId);
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
const account = await getAccountAdmin({
primaryOwnerUserId: medreportOrder.user_id,
});
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({
medusaOrder,
});
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
console.info(
`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`,
);
const idsToSend = orderedAnalysisElementsIds;
const messageXml = await composeOrderTestResponseXML({
person: {
@@ -35,8 +51,12 @@ export async function POST(request: NextRequest) {
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: idsToSend.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderedAnalysisElementsIds: idsToSend
.map(({ analysisElementId }) => analysisElementId)
.filter(Boolean) as number[],
orderedAnalysesIds: idsToSend
.map(({ analysisId }) => analysisId)
.filter(Boolean) as number[],
orderId: medreportOrder.id,
orderCreatedAt: new Date(medreportOrder.created_at),
});
@@ -44,7 +64,7 @@ export async function POST(request: NextRequest) {
try {
await sendPrivateMessageTestResponse({ messageXml });
} catch (error) {
console.error("Error sending private message test response: ", error);
console.error('Error sending private message test response: ', error);
}
}

View File

@@ -1,4 +1,5 @@
import { NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { z } from 'zod';

View File

@@ -1,10 +1,15 @@
import { NextResponse } from "next/server";
import { getAnalysisOrder } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { upsertMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
import { NextResponse } from 'next/server';
import { retrieveOrder } from '@lib/data';
import { getAccountAdmin } from '~/lib/services/account.service';
import { upsertMedipostActionLog } from '~/lib/services/medipost/medipostMessageBase.service';
import {
composeOrderTestResponseXML,
sendPrivateMessageTestResponse,
} from '~/lib/services/medipost/medipostTest.service';
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
import { getAnalysisOrder } from '~/lib/services/order.service';
export async function POST(request: Request) {
// const isDev = process.env.NODE_ENV === 'development';
@@ -14,13 +19,19 @@ export async function POST(request: Request) {
const { medusaOrderId } = await request.json();
const medusaOrder = await retrieveOrder(medusaOrderId)
const medusaOrder = await retrieveOrder(medusaOrderId);
const analysisOrder = await getAnalysisOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: analysisOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
const account = await getAccountAdmin({
primaryOwnerUserId: analysisOrder.user_id,
});
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({
medusaOrder,
});
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
console.info(
`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`,
);
const messageXml = await composeOrderTestResponseXML({
person: {
idCode: account.personal_code!,
@@ -28,8 +39,12 @@ export async function POST(request: Request) {
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderedAnalysisElementsIds: orderedAnalysisElementsIds
.map(({ analysisElementId }) => analysisElementId)
.filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElementsIds
.map(({ analysisId }) => analysisId)
.filter(Boolean) as number[],
orderId: analysisOrder.id,
orderCreatedAt: new Date(analysisOrder.created_at),
});
@@ -42,7 +57,7 @@ export async function POST(request: Request) {
});
await sendPrivateMessageTestResponse({ messageXml });
} catch (error) {
console.error("Error sending private message test response: ", error);
console.error('Error sending private message test response: ', error);
}
return NextResponse.json({ success: true });

View File

@@ -3,15 +3,13 @@ import Link from 'next/link';
import type { AuthError } from '@supabase/supabase-js';
import { ResendAuthLinkForm } from '@kit/auth/resend-email-link';
import { pathsConfig } from '@kit/shared/config';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
import { pathsConfig } from '@kit/shared/config';
import { withI18n } from '~/lib/i18n/with-i18n';
interface AuthCallbackErrorPageProps {
searchParams: Promise<{
error: string;

View File

@@ -1,5 +1,4 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '@kit/shared/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {

View File

@@ -1,17 +1,20 @@
import { redirect } from 'next/navigation';
import type { NextRequest } from 'next/server';
import { createAuthCallbackService, getErrorURLParameters } from '@kit/supabase/auth';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
import { pathsConfig } from '@kit/shared/config';
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
import {
createAuthCallbackService,
getErrorURLParameters,
} from '@kit/supabase/auth';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
const ERROR_PATH = '/auth/callback/error';
const redirectOnError = (searchParams?: string) => {
return redirect(`${ERROR_PATH}${searchParams ? `?${searchParams}` : ''}`);
}
};
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
@@ -44,15 +47,13 @@ export async function GET(request: NextRequest) {
const service = createAuthCallbackService(getSupabaseServerClient());
const oauthResult = await service.exchangeCodeForSession(authCode);
if (!("isSuccess" in oauthResult)) {
if (!('isSuccess' in oauthResult)) {
return redirectOnError(oauthResult.searchParams);
}
const api = createAccountsApi(getSupabaseServerClient());
const account = await api.getPersonalAccountByUserId(
oauthResult.user.id,
);
const account = await api.getPersonalAccountByUserId(oauthResult.user.id);
if (!account.email || !account.name || !account.last_name) {
return redirect(pathsConfig.auth.updateAccount);

View File

@@ -1,5 +1,4 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '@kit/shared/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {

View File

@@ -1,10 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { pathsConfig } from '@kit/shared/config';
import { createAuthCallbackService } from '@kit/supabase/auth';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { pathsConfig } from '@kit/shared/config';
export async function GET(request: NextRequest) {
const service = createAuthCallbackService(getSupabaseServerClient());

View File

@@ -2,11 +2,11 @@
import React from 'react';
import { pathsConfig } from '@kit/shared/config';
import { useTranslation } from 'react-i18next';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { SuccessNotification } from '@kit/notifications/components';
import { pathsConfig } from '@kit/shared/config';
const MembershipConfirmationNotification: React.FC<{
userId: string;

View File

@@ -1,6 +1,5 @@
import { withI18n } from '~/lib/i18n/with-i18n';
async function SiteLayout(props: React.PropsWithChildren) {
return (
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>

View File

@@ -1,5 +1,4 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '@kit/shared/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {

View File

@@ -1,16 +1,14 @@
import Link from 'next/link';
import { PasswordResetRequestContainer } from '@kit/auth/password-reset';
import { pathsConfig } from '@kit/shared/config';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
import { pathsConfig } from '@kit/shared/config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();

View File

@@ -1,25 +1,26 @@
'use client';
import Loading from '@/app/home/loading';
import { useEffect } from 'react';
import { getSupabaseBrowserClient } from '@/packages/supabase/src/clients/browser-client';
import { useRouter } from 'next/navigation';
import Loading from '@/app/home/loading';
import { getSupabaseBrowserClient } from '@/packages/supabase/src/clients/browser-client';
export function SignInPageClientRedirect() {
const router = useRouter();
useEffect(() => {
async function signIn() {
const { data, error } = await getSupabaseBrowserClient()
.auth
.signInWithOAuth({
const { data, error } =
await getSupabaseBrowserClient().auth.signInWithOAuth({
provider: 'keycloak',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
prompt: 'login',
},
}
},
});
if (error) {

View File

@@ -1,5 +1,4 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '@kit/shared/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {

View File

@@ -2,8 +2,9 @@ import { getServerAuthConfig, pathsConfig } from '@kit/shared/config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SignInPageClientRedirect } from './components/SignInPageClientRedirect';
import PasswordOption from './components/PasswordOption';
import { SignInPageClientRedirect } from './components/SignInPageClientRedirect';
interface SignInPageProps {
searchParams: Promise<{

View File

@@ -1,5 +1,4 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '@kit/shared/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {

View File

@@ -8,7 +8,6 @@ import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {

View File

@@ -1,6 +1,6 @@
import { z } from 'zod';
import Isikukood from 'isikukood';
import parsePhoneNumber from 'libphonenumber-js/min';
import { z } from 'zod';
const updateAccountSchema = {
firstName: z
@@ -39,14 +39,18 @@ const updateAccountSchema = {
(phone) => {
try {
const phoneNumber = parsePhoneNumber(phone);
return !!phoneNumber && phoneNumber.isValid() && phoneNumber.country === 'EE';
return (
!!phoneNumber &&
phoneNumber.isValid() &&
phoneNumber.country === 'EE'
);
} catch {
return false;
}
},
{
message: 'common:formFieldError.invalidPhoneNumber',
}
},
),
city: z.string().optional(),
weight: z
@@ -81,7 +85,12 @@ export const UpdateAccountSchemaServer = z.object({
height: updateAccountSchema.height.nullable(),
userConsent: updateAccountSchema.userConsent,
});
export const UpdateAccountSchemaClient = ({ isEmailUser }: { isEmailUser: boolean }) => z.object({
export const UpdateAccountSchemaClient = ({
isEmailUser,
}: {
isEmailUser: boolean;
}) =>
z.object({
firstName: updateAccountSchema.firstName,
lastName: updateAccountSchema.lastName,
personalCode: updateAccountSchema.personalCode,
@@ -99,4 +108,4 @@ export const UpdateAccountSchemaClient = ({ isEmailUser }: { isEmailUser: boolea
height: updateAccountSchema.height,
}),
userConsent: updateAccountSchema.userConsent,
});
});

View File

@@ -4,9 +4,8 @@ import { updateCustomer } from '@lib/data/customer';
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { pathsConfig } from '@kit/shared/config';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { UpdateAccountSchemaServer } from '../schemas/update-account.schema';
@@ -32,14 +31,14 @@ export const onUpdateAccount = enhanceAction(
phone: params.phone,
});
} catch (e) {
console.error("Failed to update Medusa customer", e);
console.error('Failed to update Medusa customer', e);
}
const hasUnseenMembershipConfirmation =
await api.hasUnseenMembershipConfirmation();
return {
hasUnseenMembershipConfirmation,
}
};
},
{
schema: UpdateAccountSchemaServer,

View File

@@ -1,6 +1,5 @@
import { withI18n } from '~/lib/i18n/with-i18n';
async function SiteLayout(props: React.PropsWithChildren) {
return (
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>

View File

@@ -1,5 +1,4 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '@kit/shared/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {

View File

@@ -1,11 +1,10 @@
import { redirect } from 'next/navigation';
import { MultiFactorChallengeContainer } from '@kit/auth/mfa';
import { pathsConfig } from '@kit/shared/config';
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { pathsConfig } from '@kit/shared/config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
@@ -41,7 +40,8 @@ async function VerifyPage(props: Props) {
}
const nextPath = (await props.searchParams).next;
const redirectPath = !!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home;
const redirectPath =
!!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home;
return (
<MultiFactorChallengeContainer

View File

@@ -130,7 +130,7 @@ const AnalysisDoctor = ({
/>
{endIcon || <div className="mx-2 w-4" />}
</>
) : (isCancelled ? null : (
) : isCancelled ? null : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">
@@ -140,7 +140,7 @@ const AnalysisDoctor = ({
<div className="mx-8 w-[60px]"></div>
<AnalysisLevelBarSkeleton />
</>
))}
)}
</div>
</div>
);

View File

@@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { ArrowDown } from 'lucide-react';
import { cn } from '@kit/ui/utils';
import { AnalysisResultForDisplay } from './analysis-doctor';
export enum AnalysisResultLevel {
@@ -48,7 +49,7 @@ const Level = ({
export const AnalysisLevelBarSkeleton = () => {
return (
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
<Level color="gray-200" />
</div>
);
@@ -65,8 +66,11 @@ const AnalysisLevelBar = ({
level: AnalysisResultLevel;
results: AnalysisResultForDisplay;
}) => {
const { norm_lower: lower, norm_upper: upper, response_value: value } = results;
const {
norm_lower: lower,
norm_upper: upper,
response_value: value,
} = results;
const arrowLocation = useMemo(() => {
if (value < lower!) {
return 0;
@@ -85,24 +89,23 @@ const AnalysisLevelBar = ({
return calculated;
}, [value, upper, lower]);
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(
() => [
level === AnalysisResultLevel.VERY_LOW,
level === AnalysisResultLevel.LOW,
level === AnalysisResultLevel.HIGH,
level === AnalysisResultLevel.VERY_HIGH,
], [level, value, upper, lower]);
],
[level, value, upper, lower],
);
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
return (
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
{normLowerIncluded && (
<>
<Level
isActive={isVeryLow}
color="destructive"
isFirst
/>
<Level isActive={isVeryLow} color="destructive" isFirst />
<Level isActive={isLow} color="warning" />
</>
)}
@@ -110,21 +113,16 @@ const AnalysisLevelBar = ({
<Level
isFirst={!normLowerIncluded}
isLast={!normUpperIncluded}
{...(hasAbnormalLevel ? { color: "warning", isActive: false } : { color: "success", isActive: true })}
{...(hasAbnormalLevel
? { color: 'warning', isActive: false }
: { color: 'success', isActive: true })}
arrowLocation={arrowLocation}
/>
{normUpperIncluded && (
<>
<Level
isActive={isHigh}
color="warning"
/>
<Level
isActive={isVeryHigh}
color="destructive"
isLast
/>
<Level isActive={isHigh} color="warning" />
<Level isActive={isVeryHigh} color="destructive" isLast />
</>
)}
</div>

View File

@@ -23,9 +23,7 @@ import {
doctorAnalysisFeedbackFormSchema,
} from '@kit/doctor/schema/doctor-analysis.schema';
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
import {
useCurrentLocaleLanguageNames
} from '@kit/shared/hooks';
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
import { getFullName } from '@kit/shared/utils';
import { useUser } from '@kit/supabase/hooks/use-user';
import { Button } from '@kit/ui/button';

View File

@@ -6,6 +6,7 @@ import {
getUserDoneResponsesAction,
getUserInProgressResponsesAction,
} from '@kit/doctor/actions/table-data-fetching-actions';
import ResultsTableWrapper from './results-table-wrapper';
export default function Dashboard() {

View File

@@ -1,8 +1,7 @@
import { notFound } from 'next/navigation';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { isDoctor } from '@kit/doctor/lib/server/utils/is-doctor';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;

View File

@@ -5,6 +5,7 @@ import { useTransition } from 'react';
import Link from 'next/link';
import { format } from 'date-fns';
import { capitalize } from 'lodash';
import { Eye } from 'lucide-react';
import { getResultSetName } from '@kit/doctor/lib/helpers';
@@ -26,7 +27,6 @@ import {
import { Trans } from '@kit/ui/trans';
import DoctorJobSelect from './doctor-job-select';
import { capitalize } from 'lodash';
export default function ResultsTable({
results = [],
@@ -189,7 +189,9 @@ export default function ResultsTable({
</TableCell>
<TableCell>
{capitalize(
languageNames.of(result?.patient?.preferred_locale ?? 'et'),
languageNames.of(
result?.patient?.preferred_locale ?? 'et',
),
)}
</TableCell>
<TableCell>

View File

@@ -50,5 +50,3 @@ async function AnalysisPage({
export default DoctorGuard(AnalysisPage);
const loadResult = cache(getAnalysisResultsForDoctor);

View File

@@ -5,7 +5,6 @@ import { cookies } from 'next/headers';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
import { DoctorSidebar } from './_components/doctor-sidebar';
import { DoctorMobileNavigation } from './_components/mobile-navigation';

View File

@@ -5,12 +5,12 @@ import Link from 'next/link';
import { ArrowLeft, MessageCircle } from 'lucide-react';
import { useCaptureException } from '@kit/monitoring/hooks';
import { RootProviders } from '@kit/shared/components/root-providers';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
import { SiteHeader } from '~/(marketing)/_components/site-header';
import { RootProviders } from '@kit/shared/components/root-providers';
const GlobalErrorPage = ({
error,

View File

@@ -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" />
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) => (
{element.results?.nestedElements?.map(
(nestedElement, nestedIndex) => (
<Analysis
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
nestedElement={nestedElement}
isNestedElement
/>
))}
),
)}
</React.Fragment>
))
) : (

View File

@@ -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 ? {
{...(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", {
<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;
@@ -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} />

View File

@@ -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}

View File

@@ -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>

View File

@@ -1,25 +1,31 @@
'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
const env = () =>
z
.object({
emailSender: z
.string({
@@ -31,15 +37,15 @@ const env = () => z
error: 'NEXT_PUBLIC_SITE_URL is required',
})
.min(1),
isEnabledDispatchOnMontonioCallback: z
.boolean({
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',
isEnabledDispatchOnMontonioCallback:
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
});
const sendEmail = async ({
@@ -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({
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,36 +113,47 @@ 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 ?? '',
partnerLocationName:
(analysisPackageOrderItem?.metadata
?.partner_location_name as string) ?? '',
analysisPackageName: analysisPackageOrderItem?.title ?? '',
}
: null,
analysisItemsOrder: Array.isArray(analysisItems) && analysisItems.length > 0
analysisItemsOrder:
Array.isArray(analysisItems) && analysisItems.length > 0
? analysisItems.map(({ product }) => ({
analysisName: product?.title ?? '',
analysisId: product?.metadata?.analysisIdOriginal as string ?? '',
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;
@@ -164,51 +182,69 @@ async function sendAnalysisPackageOrderEmail({
language,
});
} catch (error) {
console.error("Failed to send email", error);
console.error('Failed to send 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 +253,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}`);
}
}

View File

@@ -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>
);

View File

@@ -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();

View File

@@ -1,6 +1,8 @@
import MontonioCallbackClient from './client-component';
export default async function MontonioCallbackPage({ searchParams }: {
export default async function MontonioCallbackPage({
searchParams,
}: {
searchParams: Promise<{
'order-token'?: string;
}>;

View File

@@ -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
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);
return [analysisPackagesType.id, synlabAnalysisType.id].includes(
productTypeId,
);
})
: [];
const ttoServiceItems = cart?.items?.filter((item) => !synlabAnalyses.some((analysis) => analysis.id === item.id)) ?? [];
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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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} />

View File

@@ -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} />

View File

@@ -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">

View File

@@ -1,11 +1,11 @@
import { redirect } from 'next/navigation';
import { toTitleCase } from '@/lib/utils';
import { createUserAnalysesApi } from '@kit/user-analyses/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { PageBody, PageHeader } from '@kit/ui/page';
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';

View File

@@ -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,16 +92,24 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
</SelectTrigger>
<SelectContent>
{Object.entries(partnerLocations
.reduce((acc, curr) => ({
{Object.entries(
partnerLocations.reduce(
(acc, curr) => ({
...acc,
[curr.city]: [...((acc[curr.city] as typeof partnerLocations) ?? []), curr],
}), {} as Record<string, typeof partnerLocations>))
.map(([city, locations]) => (
[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>
<SelectItem key={location.name} value={location.name}>
{location.name}
</SelectItem>
))}
</SelectGroup>
))}
@@ -101,15 +119,11 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
</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>
)
);
}

View File

@@ -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 />}

View File

@@ -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>
)
);
}

View File

@@ -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>
)
);
}

View File

@@ -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>
</>
)
);
}

View File

@@ -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) {

View File

@@ -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,31 +118,31 @@ 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 && (
<>
{promotion.application_method.type ===
"percentage"
{promotion.application_method.type === 'percentage'
? `${promotion.application_method.value}%`
: convertToLocale({
amount: Number(promotion.application_method.value),
amount: Number(
promotion.application_method.value,
),
currency_code:
promotion.application_method
.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>
)
);
}

View File

@@ -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>

View File

@@ -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,
},
) => {
}) => {
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>

View File

@@ -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

View File

@@ -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';

View File

@@ -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'] => {
@@ -113,15 +114,13 @@ const cards = ({
// },
{
title: 'dashboard:smoking',
description:
isNil(smoking)
description: isNil(smoking)
? 'dashboard:respondToQuestion'
: !!smoking
: smoking
? 'common:yes'
: 'common:no',
descriptionColor: 'text-primary',
icon:
isNil(smoking) ? (
icon: isNil(smoking) ? (
<Link href={pathsConfig.app.personalAccountSettings}>
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
@@ -130,7 +129,7 @@ const cards = ({
) : null,
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
},
];
];
const IS_SHOWN_RECOMMENDATIONS = false as boolean;
@@ -150,9 +149,11 @@ export default function Dashboard({
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 });

View File

@@ -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,
};

View File

@@ -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>
);
}

View File

@@ -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,18 +55,13 @@ 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'
<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,
@@ -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>

View File

@@ -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>
)
);
}

View File

@@ -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>
)
);
}

View File

@@ -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>
)
);
}

View File

@@ -1,28 +1,33 @@
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)
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((item) => (
<OrderItem
key={item.id}
@@ -35,5 +40,5 @@ export default function OrderItems({ medusaOrder }: {
</Table>
</div>
</div>
)
);
}

View File

@@ -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');

View File

@@ -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>
)
);
}

View File

@@ -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>

View File

@@ -1,6 +1,5 @@
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

View File

@@ -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';
@@ -52,7 +52,10 @@ async function analysesLoader() {
return {
analyses:
categoryProducts?.response.products
.filter(({ status, metadata }) => status === 'published' && !!metadata?.analysisIdOriginal)
.filter(
({ status, metadata }) =>
status === 'published' && !!metadata?.analysisIdOriginal,
)
.map<OrderAnalysisCard>(
({ title, description, subtitle, variants }) => {
const variant = variants![0]!;

View File

@@ -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);

View File

@@ -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>>;

Some files were not shown because too many files have changed in this diff Show More