Merge branch 'main' into MED-57
This commit is contained in:
54
app/api/job/test-medipost-responses/route.ts
Normal file
54
app/api/job/test-medipost-responses/route.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
|
||||||
|
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
|
||||||
|
import { retrieveOrder } from "@lib/data";
|
||||||
|
import { getAccountAdmin } from "~/lib/services/account.service";
|
||||||
|
import { getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
|
||||||
|
import loadEnv from "../handler/load-env";
|
||||||
|
import validateApiKey from "../handler/validate-api-key";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
loadEnv();
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateApiKey(request);
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'QUEUED' });
|
||||||
|
|
||||||
|
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 account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||||
|
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||||
|
|
||||||
|
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
||||||
|
const idsToSend = orderedAnalysisElementsIds;
|
||||||
|
const messageXml = await composeOrderTestResponseXML({
|
||||||
|
person: {
|
||||||
|
idCode: account.personal_code!,
|
||||||
|
firstName: account.name ?? '',
|
||||||
|
lastName: account.last_name ?? '',
|
||||||
|
phone: account.phone ?? '',
|
||||||
|
},
|
||||||
|
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
|
||||||
|
orderedAnalysesIds: [],
|
||||||
|
orderId: medusaOrderId,
|
||||||
|
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info("SEND XML", messageXml);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendPrivateMessageTestResponse({ messageXml });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending private message test response: ", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { sendOrderToMedipost } from "~/lib/services/medipost.service";
|
|
||||||
|
|
||||||
export const POST = async (request: NextRequest) => {
|
|
||||||
const { medusaOrderId } = (await request.json()) as { medusaOrderId: string };
|
|
||||||
await sendOrderToMedipost({ medusaOrderId });
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
};
|
|
||||||
@@ -11,7 +11,7 @@ export async function POST(request: Request) {
|
|||||||
// return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 });
|
// return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const { medusaOrderId } = await request.json();
|
const { medusaOrderId, maxItems = null } = await request.json();
|
||||||
|
|
||||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
const medusaOrder = await retrieveOrder(medusaOrderId)
|
||||||
const medreportOrder = await getOrder({ medusaOrderId });
|
const medreportOrder = await getOrder({ medusaOrderId });
|
||||||
@@ -19,7 +19,8 @@ export async function POST(request: Request) {
|
|||||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||||
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
|
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ 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} (${maxItems ?? 'all'}) ordered analysis elements`);
|
||||||
|
const idsToSend = typeof maxItems === 'number' ? orderedAnalysisElementsIds.slice(0, maxItems) : orderedAnalysisElementsIds;
|
||||||
const messageXml = await composeOrderTestResponseXML({
|
const messageXml = await composeOrderTestResponseXML({
|
||||||
person: {
|
person: {
|
||||||
idCode: account.personal_code!,
|
idCode: account.personal_code!,
|
||||||
@@ -27,7 +28,7 @@ export async function POST(request: Request) {
|
|||||||
lastName: account.last_name ?? '',
|
lastName: account.last_name ?? '',
|
||||||
phone: account.phone ?? '',
|
phone: account.phone ?? '',
|
||||||
},
|
},
|
||||||
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId),
|
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
|
||||||
orderedAnalysesIds: [],
|
orderedAnalysesIds: [],
|
||||||
orderId: medusaOrderId,
|
orderId: medusaOrderId,
|
||||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { ArrowDown } from 'lucide-react';
|
import { ArrowDown } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
|
||||||
|
|
||||||
export enum AnalysisResultLevel {
|
export enum AnalysisResultLevel {
|
||||||
VERY_LOW = 0,
|
VERY_LOW = 0,
|
||||||
@@ -17,11 +18,13 @@ const Level = ({
|
|||||||
color,
|
color,
|
||||||
isFirst = false,
|
isFirst = false,
|
||||||
isLast = false,
|
isLast = false,
|
||||||
|
arrowLocation,
|
||||||
}: {
|
}: {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
||||||
isFirst?: boolean;
|
isFirst?: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
|
arrowLocation?: number;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -32,7 +35,10 @@ const Level = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<div className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]">
|
<div
|
||||||
|
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
||||||
|
style={{ left: `${arrowLocation}%` }}
|
||||||
|
>
|
||||||
<ArrowDown strokeWidth={2} />
|
<ArrowDown strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -52,11 +58,33 @@ const AnalysisLevelBar = ({
|
|||||||
normLowerIncluded = true,
|
normLowerIncluded = true,
|
||||||
normUpperIncluded = true,
|
normUpperIncluded = true,
|
||||||
level,
|
level,
|
||||||
|
results,
|
||||||
}: {
|
}: {
|
||||||
normLowerIncluded?: boolean;
|
normLowerIncluded?: boolean;
|
||||||
normUpperIncluded?: boolean;
|
normUpperIncluded?: boolean;
|
||||||
level: AnalysisResultLevel;
|
level: AnalysisResultLevel;
|
||||||
|
results: UserAnalysisElement;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
|
const { norm_lower: lower, norm_upper: upper, response_value: value } = results;
|
||||||
|
const arrowLocation = useMemo(() => {
|
||||||
|
if (value < lower!) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normLowerIncluded || normUpperIncluded) {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculated = ((value - lower!) / (upper! - lower!)) * 100;
|
||||||
|
|
||||||
|
if (calculated > 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return calculated;
|
||||||
|
}, [value, upper, lower]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
||||||
{normLowerIncluded && (
|
{normLowerIncluded && (
|
||||||
@@ -73,8 +101,9 @@ const AnalysisLevelBar = ({
|
|||||||
<Level
|
<Level
|
||||||
isFirst={!normLowerIncluded}
|
isFirst={!normLowerIncluded}
|
||||||
isLast={!normUpperIncluded}
|
isLast={!normUpperIncluded}
|
||||||
isActive={level === AnalysisResultLevel.NORMAL}
|
color={level === AnalysisResultLevel.NORMAL ? "success" : "warning"}
|
||||||
color="success"
|
isActive
|
||||||
|
arrowLocation={arrowLocation}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{normUpperIncluded && (
|
{normUpperIncluded && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
|
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
@@ -39,11 +39,12 @@ const Analysis = ({
|
|||||||
const normUpper = results?.norm_upper || 0;
|
const normUpper = results?.norm_upper || 0;
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const isUnderNorm = value < normLower;
|
const analysisResultLevel = useMemo(() => {
|
||||||
const getAnalysisResultLevel = () => {
|
|
||||||
if (!results) {
|
if (!results) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUnderNorm = value < normLower;
|
||||||
if (isUnderNorm) {
|
if (isUnderNorm) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case AnalysisStatus.MEDIUM:
|
case AnalysisStatus.MEDIUM:
|
||||||
@@ -60,7 +61,7 @@ const Analysis = ({
|
|||||||
default:
|
default:
|
||||||
return AnalysisResultLevel.NORMAL;
|
return AnalysisResultLevel.NORMAL;
|
||||||
}
|
}
|
||||||
};
|
}, [results, value, normLower]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-border flex flex-col items-center justify-between gap-2 rounded-lg border px-5 px-12 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
<div className="border-border flex flex-col items-center justify-between gap-2 rounded-lg border px-5 px-12 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
||||||
@@ -99,9 +100,10 @@ const Analysis = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AnalysisLevelBar
|
<AnalysisLevelBar
|
||||||
|
results={results}
|
||||||
normLowerIncluded={normLowerIncluded}
|
normLowerIncluded={normLowerIncluded}
|
||||||
normUpperIncluded={normUpperIncluded}
|
normUpperIncluded={normUpperIncluded}
|
||||||
level={getAnalysisResultLevel()!}
|
level={analysisResultLevel!}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ import {
|
|||||||
PAGE_VIEW_ACTION,
|
PAGE_VIEW_ACTION,
|
||||||
createPageViewLog,
|
createPageViewLog,
|
||||||
} from '~/lib/services/audit/pageView.service';
|
} from '~/lib/services/audit/pageView.service';
|
||||||
import { getAnalysisOrders } from '~/lib/services/order.service';
|
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
|
||||||
|
import { ButtonTooltip } from '~/components/ui/button-tooltip';
|
||||||
|
|
||||||
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
|
|
||||||
import Analysis from './_components/analysis';
|
import Analysis from './_components/analysis';
|
||||||
|
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
@@ -51,43 +52,16 @@ async function AnalysisResultsPage() {
|
|||||||
action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS,
|
action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const analysisElementIds = [
|
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
|
||||||
...new Set(
|
...new Set(analysisOrders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]),
|
||||||
analysisOrders
|
|
||||||
?.flatMap((order) => order.analysis_element_ids)
|
|
||||||
.filter(Boolean) as number[],
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
const analysisElements = await getAnalysisElements({
|
|
||||||
ids: analysisElementIds,
|
|
||||||
});
|
|
||||||
const analysisElementsWithResults =
|
|
||||||
analysisResponseElements
|
|
||||||
?.sort((a, b) => {
|
|
||||||
if (!a.response_time || !b.response_time) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
new Date(b.response_time).getTime() -
|
|
||||||
new Date(a.response_time).getTime()
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((results) => ({ results })) ?? [];
|
|
||||||
const analysisElementsWithoutResults = analysisElements.filter(
|
|
||||||
(element) =>
|
|
||||||
!analysisElementsWithResults?.some(
|
|
||||||
({ results }) =>
|
|
||||||
results.analysis_element_original_id === element.analysis_id_original,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasNoAnalysisElements =
|
const analysisElementIds = getAnalysisElementIds(analysisOrders);
|
||||||
analysisElementsWithResults.length === 0 &&
|
const analysisElements = await getAnalysisElements({ ids: analysisElementIds });
|
||||||
analysisElementsWithoutResults.length === 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<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 className="mt-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-0">
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<Trans i18nKey="analysis-results:pageTitle" />
|
<Trans i18nKey="analysis-results:pageTitle" />
|
||||||
@@ -106,33 +80,46 @@ async function AnalysisResultsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-8">
|
||||||
{analysisElementsWithResults.map(({ results }) => {
|
{analysisOrders.length > 0 && analysisElements.length > 0 ? analysisOrders.map((analysisOrder) => {
|
||||||
const analysisElement = analysisElements.find(
|
const analysisResponse = analysisResponses?.find((response) => response.analysis_order_id === analysisOrder.id);
|
||||||
(element) =>
|
const analysisElementIds = getAnalysisElementIds([analysisOrder]);
|
||||||
element.analysis_id_original ===
|
const analysisElementsForOrder = analysisElements.filter((element) => analysisElementIds.includes(element.id));
|
||||||
results.analysis_element_original_id,
|
|
||||||
);
|
|
||||||
if (!analysisElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Analysis
|
<div key={analysisOrder.id} className="flex flex-col gap-4">
|
||||||
key={results.id}
|
<h4>
|
||||||
analysisElement={analysisElement}
|
<Trans i18nKey="analysis-results:orderTitle" values={{ orderNumber: analysisOrder.medusa_order_id }} />
|
||||||
results={results}
|
</h4>
|
||||||
/>
|
<h5>
|
||||||
|
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
||||||
|
<ButtonTooltip
|
||||||
|
content={`${new Date(analysisOrder.created_at).toLocaleString()}`}
|
||||||
|
className="ml-6"
|
||||||
|
/>
|
||||||
|
</h5>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => {
|
||||||
|
const results = analysisResponse?.elements.some((element) => element.analysis_element_original_id === analysisElement.analysis_id_original)
|
||||||
|
&& analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original);
|
||||||
|
if (!results) {
|
||||||
|
return (
|
||||||
|
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} results={results} />
|
||||||
|
);
|
||||||
|
}) : (
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
<Trans i18nKey="analysis-results:noAnalysisElements" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}) : (
|
||||||
{analysisElementsWithoutResults.map((element) => (
|
|
||||||
<Analysis
|
|
||||||
key={element.analysis_id_original}
|
|
||||||
analysisElement={element}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{hasNoAnalysisElements && (
|
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
<Trans i18nKey="analysis-results:noAnalysisElements" />
|
<Trans i18nKey="analysis-results:noAnalysisOrders" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { placeOrder, retrieveCart } from "@lib/data/cart";
|
|||||||
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
||||||
import { createOrder } from '~/lib/services/order.service';
|
import { createOrder } from '~/lib/services/order.service';
|
||||||
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
|
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
|
||||||
|
import { createNotificationsApi } from '@kit/notifications/api';
|
||||||
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
|
import { AccountWithParams } from '@kit/accounts/api';
|
||||||
|
|
||||||
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
||||||
const MONTONIO_PAID_STATUS = 'PAID';
|
const MONTONIO_PAID_STATUS = 'PAID';
|
||||||
@@ -31,7 +34,22 @@ const env = () => z
|
|||||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendEmail = async ({ email, analysisPackageName, personName, partnerLocationName, language }: { email: string, analysisPackageName: string, personName: string, partnerLocationName: string, language: string }) => {
|
const sendEmail = async ({
|
||||||
|
account,
|
||||||
|
email,
|
||||||
|
analysisPackageName,
|
||||||
|
personName,
|
||||||
|
partnerLocationName,
|
||||||
|
language,
|
||||||
|
}: {
|
||||||
|
account: AccountWithParams,
|
||||||
|
email: string,
|
||||||
|
analysisPackageName: string,
|
||||||
|
personName: string,
|
||||||
|
partnerLocationName: string,
|
||||||
|
language: string,
|
||||||
|
}) => {
|
||||||
|
const client = getSupabaseServerAdminClient();
|
||||||
try {
|
try {
|
||||||
const { renderSynlabAnalysisPackageEmail } = await import('@kit/email-templates');
|
const { renderSynlabAnalysisPackageEmail } = await import('@kit/email-templates');
|
||||||
const { getMailer } = await import('@kit/mailers');
|
const { getMailer } = await import('@kit/mailers');
|
||||||
@@ -55,6 +73,11 @@ const sendEmail = async ({ email, analysisPackageName, personName, partnerLocati
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(`Failed to send email, message=${error}`);
|
throw new Error(`Failed to send email, message=${error}`);
|
||||||
});
|
});
|
||||||
|
await createNotificationsApi(client)
|
||||||
|
.createNotification({
|
||||||
|
account_id: account.id,
|
||||||
|
body: html,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to send email, message=${error}`);
|
throw new Error(`Failed to send email, message=${error}`);
|
||||||
}
|
}
|
||||||
@@ -62,13 +85,13 @@ const sendEmail = async ({ email, analysisPackageName, personName, partnerLocati
|
|||||||
|
|
||||||
export async function processMontonioCallback(orderToken: string) {
|
export async function processMontonioCallback(orderToken: string) {
|
||||||
const { language } = await createI18nServerInstance();
|
const { language } = await createI18nServerInstance();
|
||||||
|
|
||||||
const secretKey = process.env.MONTONIO_SECRET_KEY as string;
|
const secretKey = process.env.MONTONIO_SECRET_KEY as string;
|
||||||
|
|
||||||
const decoded = jwt.verify(orderToken, secretKey, {
|
const decoded = jwt.verify(orderToken, secretKey, {
|
||||||
algorithms: ['HS256'],
|
algorithms: ['HS256'],
|
||||||
}) as MontonioOrderToken;
|
}) as MontonioOrderToken;
|
||||||
|
|
||||||
if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) {
|
if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) {
|
||||||
throw new Error("Payment not successful");
|
throw new Error("Payment not successful");
|
||||||
}
|
}
|
||||||
@@ -79,7 +102,7 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [,, cartId] = decoded.merchantReferenceDisplay.split(':');
|
const [, , cartId] = decoded.merchantReferenceDisplay.split(':');
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
throw new Error("Cart ID not found");
|
throw new Error("Cart ID not found");
|
||||||
}
|
}
|
||||||
@@ -89,6 +112,7 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
throw new Error("Cart not found");
|
throw new Error("Cart not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
|
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
|
||||||
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||||
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
|
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
|
||||||
@@ -96,7 +120,7 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
const { productTypes } = await listProductTypes();
|
const { productTypes } = await listProductTypes();
|
||||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
|
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
|
||||||
const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id);
|
const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id);
|
||||||
|
|
||||||
const orderResult = {
|
const orderResult = {
|
||||||
medusaOrderId: medusaOrder.id,
|
medusaOrderId: medusaOrder.id,
|
||||||
email: medusaOrder.email,
|
email: medusaOrder.email,
|
||||||
@@ -107,10 +131,10 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
|
|
||||||
const { medusaOrderId, email, partnerLocationName, analysisPackageName } = orderResult;
|
const { medusaOrderId, email, partnerLocationName, analysisPackageName } = orderResult;
|
||||||
const personName = account.name;
|
const personName = account.name;
|
||||||
|
|
||||||
if (email && analysisPackageName) {
|
if (email && analysisPackageName) {
|
||||||
try {
|
try {
|
||||||
await sendEmail({ email, analysisPackageName, personName, partnerLocationName, language });
|
await sendEmail({ account, email, analysisPackageName, personName, partnerLocationName, language });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send email", error);
|
console.error("Failed to send email", error);
|
||||||
}
|
}
|
||||||
@@ -118,9 +142,9 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
// @TODO send email for separate analyses
|
// @TODO send email for separate analyses
|
||||||
console.error("Missing email or analysisPackageName", orderResult);
|
console.error("Missing email or analysisPackageName", orderResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||||
|
|
||||||
return { success: true, orderId };
|
return { success: true, orderId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to place order", error);
|
console.error("Failed to place order", error);
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ export default async function CartPage() {
|
|||||||
|
|
||||||
const otherItemsSorted = otherItems.sort((a, b) => (a.updated_at ?? "") > (b.updated_at ?? "") ? -1 : 1);
|
const otherItemsSorted = otherItems.sort((a, b) => (a.updated_at ?? "") > (b.updated_at ?? "") ? -1 : 1);
|
||||||
const item = otherItemsSorted[0];
|
const item = otherItemsSorted[0];
|
||||||
|
const hasItemsWithTimer = false as boolean;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<PageHeader title={<Trans i18nKey="cart:title" />}>
|
<PageHeader title={<Trans i18nKey="cart:title" />}>
|
||||||
{item && item.updated_at && <CartTimer cartItem={item} />}
|
{hasItemsWithTimer && item && item.updated_at && <CartTimer cartItem={item} />}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Cart cart={cart} analysisPackages={analysisPackages} otherItems={otherItems} />
|
<Cart cart={cart} analysisPackages={analysisPackages} otherItems={otherItems} />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||||
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
||||||
|
import { createPageViewLog, PAGE_VIEW_ACTION } from '~/lib/services/audit/pageView.service';
|
||||||
|
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
@@ -15,8 +17,18 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function OrderAnalysisPage() {
|
async function OrderAnalysisPage() {
|
||||||
|
const account = await loadCurrentUserAccount();
|
||||||
|
if (!account) {
|
||||||
|
throw new Error('Account not found');
|
||||||
|
}
|
||||||
|
|
||||||
const { analyses, countryCode } = await loadAnalyses();
|
const { analyses, countryCode } = await loadAnalyses();
|
||||||
|
|
||||||
|
await createPageViewLog({
|
||||||
|
accountId: account.id,
|
||||||
|
action: PAGE_VIEW_ACTION.VIEW_ORDER_ANALYSIS,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HomeLayoutPageHeader
|
<HomeLayoutPageHeader
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { PageBody } from '@kit/ui/makerkit/page';
|
|||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||||
import OrdersTable from '../../_components/orders/orders-table';
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import type { IOrderLineItem } from '../../_components/orders/types';
|
|
||||||
import { getAnalysisOrders } from '~/lib/services/order.service';
|
import { getAnalysisOrders } from '~/lib/services/order.service';
|
||||||
|
import OrderBlock from '../../_components/orders/order-block';
|
||||||
|
import React from 'react';
|
||||||
|
import { Divider } from '@medusajs/ui';
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
@@ -29,42 +30,7 @@ async function OrdersPage() {
|
|||||||
redirect(pathsConfig.auth.signIn);
|
redirect(pathsConfig.auth.signIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages')!;
|
||||||
const analysisPackageOrders: IOrderLineItem[] = medusaOrders.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
|
||||||
?.filter((item) => item.product_type_id === analysisPackagesType?.id)
|
|
||||||
.map((item) => {
|
|
||||||
const localOrder = analysisOrders.find((order) => order.medusa_order_id === id);
|
|
||||||
if (!localOrder) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
item,
|
|
||||||
medusaOrderId: id,
|
|
||||||
orderId: localOrder?.id,
|
|
||||||
orderStatus: localOrder.status,
|
|
||||||
analysis_element_ids: localOrder.analysis_element_ids,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((order) => order !== null)
|
|
||||||
|| []);
|
|
||||||
|
|
||||||
const otherOrders: IOrderLineItem[] = medusaOrders
|
|
||||||
.filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id))
|
|
||||||
.flatMap(({ id, items, payment_status, fulfillment_status }) => items
|
|
||||||
?.map((item) => {
|
|
||||||
const analysisOrder = analysisOrders.find((order) => order.medusa_order_id === id);
|
|
||||||
if (!analysisOrder) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
item,
|
|
||||||
medusaOrderId: id,
|
|
||||||
orderId: analysisOrder.id,
|
|
||||||
orderStatus: analysisOrder.status,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((order) => order !== null)
|
|
||||||
|| []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -73,8 +39,27 @@ async function OrdersPage() {
|
|||||||
description={<Trans i18nKey={'orders:description'} />}
|
description={<Trans i18nKey={'orders:description'} />}
|
||||||
/>
|
/>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<OrdersTable orderItems={analysisPackageOrders} title="orders:table.analysisPackage" />
|
{analysisOrders.map((analysisOrder) => {
|
||||||
<OrdersTable orderItems={otherOrders} title="orders:table.otherOrders" />
|
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);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={analysisOrder.id}>
|
||||||
|
<Divider className="my-6" />
|
||||||
|
<OrderBlock
|
||||||
|
analysisOrder={analysisOrder}
|
||||||
|
itemsAnalysisPackage={medusaOrderItemsAnalysisPackages}
|
||||||
|
itemsOther={medusaOrderItemsOther}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { toTitleCase } from '@/lib/utils';
|
|||||||
|
|
||||||
import Dashboard from '../_components/dashboard';
|
import Dashboard from '../_components/dashboard';
|
||||||
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
|
||||||
|
import DashboardCards from '../_components/dashboard-cards';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
@@ -26,6 +27,7 @@ async function UserHomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DashboardCards />
|
||||||
<PageHeader title={
|
<PageHeader title={
|
||||||
<>
|
<>
|
||||||
<Trans i18nKey={'common:welcome'} />
|
<Trans i18nKey={'common:welcome'} />
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { deleteLineItem } from "@lib/data/cart";
|
|
||||||
import { Spinner } from "@medusajs/icons";
|
import { Spinner } from "@medusajs/icons";
|
||||||
|
import { handleDeleteCartItem } from "~/lib/services/medusaCart.service";
|
||||||
|
|
||||||
const CartItemDelete = ({
|
const CartItemDelete = ({
|
||||||
id,
|
id,
|
||||||
@@ -22,7 +22,7 @@ const CartItemDelete = ({
|
|||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
|
||||||
const promise = async () => {
|
const promise = async () => {
|
||||||
await deleteLineItem(id);
|
await handleDeleteCartItem({ lineId: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
|
|||||||
45
app/home/(user)/_components/dashboard-cards.tsx
Normal file
45
app/home/(user)/_components/dashboard-cards.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
} from '@kit/ui/card';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { ChevronRight, HeartPulse } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function DashboardCards() {
|
||||||
|
return (
|
||||||
|
<div className='flex gap-4 lg:px-4'>
|
||||||
|
<Card
|
||||||
|
variant="gradient-success"
|
||||||
|
className="flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<CardHeader className="flex-row">
|
||||||
|
<div
|
||||||
|
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
||||||
|
>
|
||||||
|
<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'>
|
||||||
|
<Link href='/home/order-analysis'>
|
||||||
|
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||||
|
<ChevronRight className="size-4 stroke-2" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="flex flex-col items-start gap-2">
|
||||||
|
<h5>
|
||||||
|
<Trans i18nKey='dashboard:heroCard.orderAnalysis.title' />
|
||||||
|
</h5>
|
||||||
|
<CardDescription className="text-primary">
|
||||||
|
<Trans i18nKey='dashboard:heroCard.orderAnalysis.description' />
|
||||||
|
</CardDescription>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,11 +7,14 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
|
CardDescription,
|
||||||
} from '@kit/ui/card';
|
} from '@kit/ui/card';
|
||||||
import { StoreProduct, StoreProductVariant } from '@medusajs/types';
|
import { StoreProduct, StoreProductVariant } from '@medusajs/types';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { InfoTooltip } from '~/components/ui/info-tooltip';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export default function OrderAnalysesCards({
|
export default function OrderAnalysesCards({
|
||||||
analyses,
|
analyses,
|
||||||
@@ -21,7 +24,7 @@ export default function OrderAnalysesCards({
|
|||||||
countryCode: string;
|
countryCode: string;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
||||||
const handleSelect = async (selectedVariant: StoreProductVariant) => {
|
const handleSelect = async (selectedVariant: StoreProductVariant) => {
|
||||||
if (!selectedVariant?.id || isAddingToCart) return null
|
if (!selectedVariant?.id || isAddingToCart) return null
|
||||||
@@ -44,37 +47,62 @@ export default function OrderAnalysesCards({
|
|||||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
<div className="grid grid-cols-3 gap-6 mt-4">
|
||||||
{analyses.map(({
|
{analyses.map(({
|
||||||
title,
|
title,
|
||||||
variants
|
variants,
|
||||||
}) => (
|
description,
|
||||||
<Card
|
subtitle,
|
||||||
key={title}
|
status,
|
||||||
variant="gradient-success"
|
metadata,
|
||||||
className="flex flex-col justify-between"
|
}) => {
|
||||||
>
|
const isAvailable = status === 'published' && !!metadata?.analysisIdOriginal;
|
||||||
<CardHeader className="items-end-safe">
|
return (
|
||||||
<div className='flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
<Card
|
||||||
<Button
|
key={title}
|
||||||
size="icon"
|
variant={isAvailable ? "gradient-success" : "gradient-warning"}
|
||||||
variant="outline"
|
className="flex flex-col justify-between"
|
||||||
className="px-2 text-black"
|
>
|
||||||
onClick={() => handleSelect(variants![0]!)}
|
<CardHeader className="flex-row">
|
||||||
|
<div
|
||||||
|
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
||||||
>
|
>
|
||||||
{isAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
<HeartPulse className="size-4 fill-green-500" />
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
{isAvailable && (
|
||||||
</CardHeader>
|
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
||||||
<CardFooter className="flex flex-col items-start gap-2">
|
<Button
|
||||||
<div
|
size="icon"
|
||||||
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
variant="outline"
|
||||||
>
|
className="px-2 text-black"
|
||||||
<HeartPulse className="size-4 fill-green-500" />
|
onClick={() => handleSelect(variants![0]!)}
|
||||||
</div>
|
>
|
||||||
<h5>
|
{isAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||||
{title}
|
</Button>
|
||||||
</h5>
|
</div>
|
||||||
</CardFooter>
|
)}
|
||||||
</Card>
|
</CardHeader>
|
||||||
))}
|
<CardFooter className="flex flex-col items-start gap-2">
|
||||||
|
<h5>
|
||||||
|
{title}
|
||||||
|
{description && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<InfoTooltip content={`${description}`} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</h5>
|
||||||
|
{isAvailable && subtitle && (
|
||||||
|
<CardDescription>
|
||||||
|
{subtitle}
|
||||||
|
</CardDescription>
|
||||||
|
)}
|
||||||
|
{!isAvailable && (
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey={'order-analysis:analysisNotAvailable'} />
|
||||||
|
</CardDescription>
|
||||||
|
)}
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
app/home/(user)/_components/orders/order-block.tsx
Normal file
36
app/home/(user)/_components/orders/order-block.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { AnalysisOrder } from "~/lib/services/order.service";
|
||||||
|
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";
|
||||||
|
|
||||||
|
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 }} />
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<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} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
app/home/(user)/_components/orders/order-items-table.tsx
Normal file
77
app/home/(user)/_components/orders/order-items-table.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableHeader,
|
||||||
|
TableCell,
|
||||||
|
} from '@kit/ui/table';
|
||||||
|
import { StoreOrderLineItem } from "@medusajs/types";
|
||||||
|
import { AnalysisOrder } from '~/lib/services/order.service';
|
||||||
|
import { formatDate } from 'date-fns';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Eye } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function OrderItemsTable({ items, title, analysisOrder }: {
|
||||||
|
items: StoreOrderLineItem[];
|
||||||
|
title: string;
|
||||||
|
analysisOrder: AnalysisOrder;
|
||||||
|
}) {
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table className="rounded-lg border border-separate">
|
||||||
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey={title} />
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey="orders:table.createdAt" />
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey="orders:table.status" />
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="px-6">
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items
|
||||||
|
.sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1)
|
||||||
|
.map((orderItem) => (
|
||||||
|
<TableRow className="w-full" key={orderItem.id}>
|
||||||
|
<TableCell className="text-left w-[100%] px-6">
|
||||||
|
<p className="txt-medium-plus text-ui-fg-base">
|
||||||
|
{orderItem.product_title}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="px-6 whitespace-nowrap">
|
||||||
|
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="px-6 min-w-[180px]">
|
||||||
|
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right px-6">
|
||||||
|
<span className="flex gap-x-1 justify-end w-[30px]">
|
||||||
|
<Link href={`/home/analysis-results`} 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"
|
||||||
|
>
|
||||||
|
<Eye />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import {
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
} from '@kit/ui/table';
|
|
||||||
import { Eye } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { formatDate } from "date-fns";
|
|
||||||
import { IOrderLineItem } from "./types";
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
export default function OrdersItem({ orderItem }: {
|
|
||||||
orderItem: IOrderLineItem,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<TableRow className="w-full">
|
|
||||||
<TableCell className="text-left w-[100%] px-6">
|
|
||||||
<p className="txt-medium-plus text-ui-fg-base">
|
|
||||||
{orderItem.item.product_title}
|
|
||||||
</p>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
|
||||||
{formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')}
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
|
||||||
<Trans i18nKey={`orders:status.${orderItem.orderStatus}`} />
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="text-right px-6">
|
|
||||||
<span className="flex gap-x-1 justify-end w-[60px]">
|
|
||||||
<Link href={`/home/order/${orderItem.orderId}`} 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"
|
|
||||||
>
|
|
||||||
<Eye />
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableHeader,
|
|
||||||
} from '@kit/ui/table';
|
|
||||||
import OrdersItem from "./orders-item";
|
|
||||||
import { IOrderLineItem } from "./types";
|
|
||||||
|
|
||||||
export default function OrdersTable({ orderItems, title }: {
|
|
||||||
orderItems: IOrderLineItem[];
|
|
||||||
title: string;
|
|
||||||
}) {
|
|
||||||
if (!orderItems || orderItems.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table className="rounded-lg border border-separate">
|
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="px-6">
|
|
||||||
<Trans i18nKey={title} />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="px-6">
|
|
||||||
<Trans i18nKey="orders:table.createdAt" />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="px-6">
|
|
||||||
<Trans i18nKey="orders:table.status" />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="px-6">
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{orderItems
|
|
||||||
.sort((a, b) => (a.item.created_at ?? "") > (b.item.created_at ?? "") ? -1 : 1)
|
|
||||||
.map((orderItem) => (<OrdersItem key={orderItem.item.id} orderItem={orderItem} />))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { StoreOrderLineItem } from "@medusajs/types";
|
|
||||||
|
|
||||||
export interface IOrderLineItem {
|
|
||||||
item: StoreOrderLineItem;
|
|
||||||
medusaOrderId: string;
|
|
||||||
orderId: number;
|
|
||||||
orderStatus: string;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { User } from '@supabase/supabase-js';
|
import type { User } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { ApplicationRole } from '@kit/accounts/types/accounts';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -18,6 +19,7 @@ type AccountModel = {
|
|||||||
label: string | null;
|
label: string | null;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
|
application_role: ApplicationRole | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TeamAccountLayoutSidebar(props: {
|
export function TeamAccountLayoutSidebar(props: {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { useMemo } from 'react';
|
||||||
BorderedNavigationMenu,
|
|
||||||
BorderedNavigationMenuItem,
|
|
||||||
} from '@kit/ui/bordered-navigation-menu';
|
|
||||||
|
|
||||||
import { AppLogo } from '~/components/app-logo';
|
import { AppLogo } from '~/components/app-logo';
|
||||||
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||||
@@ -10,18 +7,22 @@ import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.co
|
|||||||
// local imports
|
// local imports
|
||||||
import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader';
|
import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader';
|
||||||
import { TeamAccountNotifications } from './team-account-notifications';
|
import { TeamAccountNotifications } from './team-account-notifications';
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export function TeamAccountNavigationMenu(props: {
|
export function TeamAccountNavigationMenu(props: {
|
||||||
workspace: TeamAccountWorkspace;
|
workspace: TeamAccountWorkspace;
|
||||||
}) {
|
}) {
|
||||||
const { account, user, accounts: rawAccounts } = props.workspace;
|
const { account, user, accounts: rawAccounts } = props.workspace;
|
||||||
|
|
||||||
const accounts = useMemo(() => rawAccounts.map((account) => ({
|
const accounts = useMemo(
|
||||||
label: account.name,
|
() =>
|
||||||
value: account.slug,
|
rawAccounts.map((account) => ({
|
||||||
image: account.picture_url,
|
label: account.name,
|
||||||
})),[rawAccounts])
|
value: account.slug,
|
||||||
|
image: account.picture_url,
|
||||||
|
application_role: account.application_role,
|
||||||
|
})),
|
||||||
|
[rawAccounts],
|
||||||
|
);
|
||||||
|
|
||||||
const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce<
|
const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce<
|
||||||
Array<{
|
Array<{
|
||||||
@@ -48,7 +49,7 @@ export function TeamAccountNavigationMenu(props: {
|
|||||||
<AppLogo />
|
<AppLogo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex items-center justify-end space-x-2.5 gap-2'}>
|
<div className={'flex items-center justify-end gap-2 space-x-2.5'}>
|
||||||
<TeamAccountNotifications accountId={account.id} userId={user.id} />
|
<TeamAccountNotifications accountId={account.id} userId={user.id} />
|
||||||
<ProfileAccountDropdownContainer
|
<ProfileAccountDropdownContainer
|
||||||
user={user}
|
user={user}
|
||||||
|
|||||||
@@ -45,11 +45,14 @@ function SidebarLayout({
|
|||||||
const data = use(loadTeamWorkspace(account));
|
const data = use(loadTeamWorkspace(account));
|
||||||
const state = use(getLayoutState(account));
|
const state = use(getLayoutState(account));
|
||||||
|
|
||||||
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({
|
const accounts = data.accounts.map(
|
||||||
label: name,
|
({ name, slug, picture_url, application_role }) => ({
|
||||||
value: slug,
|
label: name,
|
||||||
image: picture_url,
|
value: slug,
|
||||||
}));
|
image: picture_url,
|
||||||
|
application_role,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TeamAccountWorkspaceContextProvider value={data}>
|
<TeamAccountWorkspaceContextProvider value={data}>
|
||||||
@@ -91,11 +94,14 @@ function HeaderLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
const data = use(loadTeamWorkspace(account));
|
const data = use(loadTeamWorkspace(account));
|
||||||
|
|
||||||
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({
|
const accounts = data.accounts.map(
|
||||||
label: name,
|
({ name, slug, picture_url, application_role }) => ({
|
||||||
value: slug,
|
label: name,
|
||||||
image: picture_url,
|
value: slug,
|
||||||
}));
|
image: picture_url,
|
||||||
|
application_role,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TeamAccountWorkspaceContextProvider value={data}>
|
<TeamAccountWorkspaceContextProvider value={data}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import type { User } from '@supabase/supabase-js';
|
import type { User } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||||
|
import { ApplicationRole } from '@kit/accounts/types/accounts';
|
||||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
|
|
||||||
@@ -28,13 +29,13 @@ export function ProfileAccountDropdownContainer(props: {
|
|||||||
id: string | null;
|
id: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
picture_url: string | null;
|
picture_url: string | null;
|
||||||
application_role: string;
|
application_role: ApplicationRole | null;
|
||||||
};
|
};
|
||||||
accounts: {
|
accounts: {
|
||||||
label: string | null;
|
label: string | null;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
application_role: string;
|
application_role: ApplicationRole | null;
|
||||||
}[];
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const signOut = useSignOut();
|
const signOut = useSignOut();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|||||||
export enum PAGE_VIEW_ACTION {
|
export enum PAGE_VIEW_ACTION {
|
||||||
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
|
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
|
||||||
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
|
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
|
||||||
|
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createPageViewLog = async ({
|
export const createPageViewLog = async ({
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { uniqBy } from 'lodash';
|
|||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { createAnalysisGroup } from './analysis-group.service';
|
import { createAnalysisGroup } from './analysis-group.service';
|
||||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||||
import { getOrder, updateOrder } from './order.service';
|
import { getOrder, updateOrderStatus } from './order.service';
|
||||||
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
||||||
import { getAnalyses } from './analyses.service';
|
import { getAnalyses } from './analyses.service';
|
||||||
import { getAccountAdmin } from './account.service';
|
import { getAccountAdmin } from './account.service';
|
||||||
@@ -218,24 +218,27 @@ export async function readPrivateMessageResponse({
|
|||||||
privateMessage.messageId,
|
privateMessage.messageId,
|
||||||
);
|
);
|
||||||
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
||||||
|
const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId;
|
||||||
|
|
||||||
if (!messageResponse) {
|
if (!messageResponse) {
|
||||||
throw new Error(`Private message response has no results yet`);
|
throw new Error(`Private message response has no results yet for order=${medusaOrderId}`);
|
||||||
}
|
}
|
||||||
console.info(`Private message content: ${JSON.stringify(privateMessageContent)}`);
|
|
||||||
|
|
||||||
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
try {
|
try {
|
||||||
order = await getOrder({ medusaOrderId: messageResponse.ValisTellimuseId });
|
order = await getOrder({ medusaOrderId });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await deletePrivateMessage(privateMessage.messageId);
|
await deletePrivateMessage(privateMessage.messageId);
|
||||||
throw new Error(`Order not found by Medipost message ValisTellimuseId=${messageResponse.ValisTellimuseId}`);
|
throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await syncPrivateMessage({ messageResponse, order });
|
const status = await syncPrivateMessage({ messageResponse, order });
|
||||||
|
|
||||||
if (status === 'COMPLETED') {
|
if (status.isPartial) {
|
||||||
await updateOrder({ orderId: order.id, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
await updateOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' });
|
||||||
|
messageIdProcessed = privateMessage.messageId;
|
||||||
|
} else if (status.isCompleted) {
|
||||||
|
await updateOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
||||||
await deletePrivateMessage(privateMessage.messageId);
|
await deletePrivateMessage(privateMessage.messageId);
|
||||||
messageIdProcessed = privateMessage.messageId;
|
messageIdProcessed = privateMessage.messageId;
|
||||||
}
|
}
|
||||||
@@ -559,11 +562,11 @@ function getLatestMessage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncPrivateMessage({
|
async function syncPrivateMessage({
|
||||||
messageResponse,
|
messageResponse,
|
||||||
order,
|
order,
|
||||||
}: {
|
}: {
|
||||||
messageResponse: MedipostOrderResponse['Saadetis']['Vastus'];
|
messageResponse: NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>;
|
||||||
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
}) {
|
}) {
|
||||||
const supabase = getSupabaseServerAdminClient()
|
const supabase = getSupabaseServerAdminClient()
|
||||||
@@ -606,6 +609,9 @@ export async function syncPrivateMessage({
|
|||||||
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
|
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
|
||||||
'id' | 'created_at' | 'updated_at'
|
'id' | 'created_at' | 'updated_at'
|
||||||
>[] = [];
|
>[] = [];
|
||||||
|
|
||||||
|
const analysisResponseId = analysisResponse[0]!.id;
|
||||||
|
|
||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
const groupItems = toArray(
|
const groupItems = toArray(
|
||||||
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
||||||
@@ -618,7 +624,7 @@ export async function syncPrivateMessage({
|
|||||||
responses.push(
|
responses.push(
|
||||||
...elementAnalysisResponses.map((response) => ({
|
...elementAnalysisResponses.map((response) => ({
|
||||||
analysis_element_original_id: element.UuringId,
|
analysis_element_original_id: element.UuringId,
|
||||||
analysis_response_id: analysisResponse[0]!.id,
|
analysis_response_id: analysisResponseId,
|
||||||
norm_lower: response.NormAlum?.['#text'] ?? null,
|
norm_lower: response.NormAlum?.['#text'] ?? null,
|
||||||
norm_lower_included:
|
norm_lower_included:
|
||||||
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
||||||
@@ -640,11 +646,11 @@ export async function syncPrivateMessage({
|
|||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_response_elements')
|
.from('analysis_response_elements')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('analysis_response_id', analysisResponse[0].id);
|
.eq('analysis_response_id', analysisResponseId);
|
||||||
|
|
||||||
if (deleteError) {
|
if (deleteError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to clean up response elements for response id ${analysisResponse[0].id}`,
|
`Failed to clean up response elements for response id ${analysisResponseId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,12 +661,23 @@ export async function syncPrivateMessage({
|
|||||||
|
|
||||||
if (elementInsertError) {
|
if (elementInsertError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to insert order response elements for response id ${analysisResponse[0].id}`,
|
`Failed to insert order response elements for response id ${analysisResponseId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info("status", AnalysisOrderStatus[messageResponse.TellimuseOlek], messageResponse.TellimuseOlek);
|
const { data: allOrderResponseElements} = await supabase
|
||||||
return AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
.schema('medreport')
|
||||||
|
.from('analysis_response_elements')
|
||||||
|
.select('*')
|
||||||
|
.eq('analysis_response_id', analysisResponseId)
|
||||||
|
.throwOnError();
|
||||||
|
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
|
||||||
|
if (allOrderResponseElements.length !== expectedOrderResponseElements) {
|
||||||
|
return { isPartial: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
||||||
|
return { isCompleted: statusFromResponse === 'COMPLETED' };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendOrderToMedipost({
|
export async function sendOrderToMedipost({
|
||||||
@@ -688,7 +705,7 @@ export async function sendOrderToMedipost({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await sendPrivateMessage(orderXml);
|
await sendPrivateMessage(orderXml);
|
||||||
await updateOrder({ orderId: medreportOrder.id, orderStatus: 'PROCESSING' });
|
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrderedAnalysisElementsIds({
|
export async function getOrderedAnalysisElementsIds({
|
||||||
@@ -720,7 +737,7 @@ export async function getOrderedAnalysisElementsIds({
|
|||||||
countryCode,
|
countryCode,
|
||||||
queryParams: { limit: 100, id: orderedPackageIds },
|
queryParams: { limit: 100, id: orderedPackageIds },
|
||||||
});
|
});
|
||||||
console.info(`Order has ${orderedPackagesProducts.length} packages`);
|
console.info(`Order has ${orderedPackagesProducts.length} packages = ${JSON.stringify(orderedPackageIds, null, 2)}`);
|
||||||
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
||||||
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export async function composeOrderTestResponseXML({
|
|||||||
// 1 – Järjekorras, 2 – Ootel, 3 - Töös, 4 – Lõpetatud,
|
// 1 – Järjekorras, 2 – Ootel, 3 - Töös, 4 – Lõpetatud,
|
||||||
// 5 – Tagasi lükatud, 6 – Tühistatud.
|
// 5 – Tagasi lükatud, 6 – Tühistatud.
|
||||||
const orderStatus = 4;
|
const orderStatus = 4;
|
||||||
const orderNumber = 'TSU000001200';
|
const orderNumber = orderId;
|
||||||
|
|
||||||
const allAnalysisElementsForGroups = analysisElements?.filter((element) => {
|
const allAnalysisElementsForGroups = analysisElements?.filter((element) => {
|
||||||
return analysisGroups.some((group) => group.id === element.analysis_groups.id);
|
return analysisGroups.some((group) => group.id === element.analysis_groups.id);
|
||||||
@@ -153,7 +153,7 @@ export async function composeOrderTestResponseXML({
|
|||||||
|
|
||||||
const lower = getRandomInt(0, 100);
|
const lower = getRandomInt(0, 100);
|
||||||
const upper = getRandomInt(lower + 1, 500);
|
const upper = getRandomInt(lower + 1, 500);
|
||||||
const result = getRandomInt(lower, upper);
|
const result = getRandomInt(lower - Math.floor(lower * 0.1), upper + Math.floor(upper * 0.1));
|
||||||
addedIds.add(relatedAnalysisElement.id);
|
addedIds.add(relatedAnalysisElement.id);
|
||||||
return (`
|
return (`
|
||||||
<UuringuGrupp>
|
<UuringuGrupp>
|
||||||
@@ -175,7 +175,7 @@ export async function composeOrderTestResponseXML({
|
|||||||
<VastuseAeg>${formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')}</VastuseAeg>
|
<VastuseAeg>${formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')}</VastuseAeg>
|
||||||
<NormYlem kaasaarvatud=\"EI\">${upper}</NormYlem>
|
<NormYlem kaasaarvatud=\"EI\">${upper}</NormYlem>
|
||||||
<NormAlum kaasaarvatud=\"EI\">${lower}</NormAlum>
|
<NormAlum kaasaarvatud=\"EI\">${lower}</NormAlum>
|
||||||
<NormiStaatus>0</NormiStaatus>
|
<NormiStaatus>${result < lower ? 1 : (result > upper ? 1 : 0)}</NormiStaatus>
|
||||||
<ProoviJarjenumber>1</ProoviJarjenumber>
|
<ProoviJarjenumber>1</ProoviJarjenumber>
|
||||||
</UuringuVastus>
|
</UuringuVastus>
|
||||||
</UuringuElement>
|
</UuringuElement>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
|||||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
||||||
|
import { getCartId } from '@lib/data/cookies';
|
||||||
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
||||||
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
||||||
import { requireUserInServerComponent } from '../server/require-user-in-server-component';
|
import { requireUserInServerComponent } from '../server/require-user-in-server-component';
|
||||||
@@ -64,6 +65,36 @@ export async function handleAddToCart({
|
|||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleDeleteCartItem({
|
||||||
|
lineId,
|
||||||
|
}: {
|
||||||
|
lineId: string;
|
||||||
|
}) {
|
||||||
|
await deleteLineItem(lineId);
|
||||||
|
|
||||||
|
const supabase = getSupabaseServerClient();
|
||||||
|
const cartId = await getCartId();
|
||||||
|
const user = await requireUserInServerComponent();
|
||||||
|
const account = await loadCurrentUserAccount()
|
||||||
|
if (!account) {
|
||||||
|
throw new Error('Account not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.schema('audit')
|
||||||
|
.from('cart_entries')
|
||||||
|
.insert({
|
||||||
|
variant_id: lineId,
|
||||||
|
operation: 'REMOVE_FROM_CART',
|
||||||
|
account_id: account.id,
|
||||||
|
cart_id: cartId!,
|
||||||
|
changed_by: user.id,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw new Error('Error logging cart entry: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleNavigateToPayment({ language, paymentSessionId }: { language: string, paymentSessionId: string }) {
|
export async function handleNavigateToPayment({ language, paymentSessionId }: { language: string, paymentSessionId: string }) {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const user = await requireUserInServerComponent();
|
const user = await requireUserInServerComponent();
|
||||||
|
|||||||
@@ -56,6 +56,30 @@ export async function updateOrder({
|
|||||||
.throwOnError();
|
.throwOnError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateOrderStatus({
|
||||||
|
orderId,
|
||||||
|
medusaOrderId,
|
||||||
|
orderStatus,
|
||||||
|
}: {
|
||||||
|
orderId?: number;
|
||||||
|
medusaOrderId?: string;
|
||||||
|
orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||||
|
}) {
|
||||||
|
const orderIdParam = orderId;
|
||||||
|
const medusaOrderIdParam = medusaOrderId;
|
||||||
|
if (!orderIdParam && !medusaOrderIdParam) {
|
||||||
|
throw new Error('Either orderId or medusaOrderId must be provided');
|
||||||
|
}
|
||||||
|
await getSupabaseServerAdminClient()
|
||||||
|
.schema('medreport')
|
||||||
|
.rpc('update_analysis_order_status', {
|
||||||
|
order_id: orderIdParam ?? -1,
|
||||||
|
status_param: orderStatus,
|
||||||
|
medusa_order_id_param: medusaOrderIdParam ?? '',
|
||||||
|
})
|
||||||
|
.throwOnError();
|
||||||
|
}
|
||||||
|
|
||||||
export async function getOrder({
|
export async function getOrder({
|
||||||
medusaOrderId,
|
medusaOrderId,
|
||||||
orderId,
|
orderId,
|
||||||
@@ -84,13 +108,39 @@ export async function getAnalysisOrders({
|
|||||||
}: {
|
}: {
|
||||||
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const query = getSupabaseServerClient()
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await client.auth.getUser();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_orders')
|
||||||
|
.select('*')
|
||||||
|
.eq("user_id", user.id)
|
||||||
|
if (orderStatus) {
|
||||||
|
query.eq('status', orderStatus);
|
||||||
|
}
|
||||||
|
const orders = await query.order('created_at', { ascending: false }).throwOnError();
|
||||||
|
return orders.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAnalysisOrdersAdmin({
|
||||||
|
orderStatus,
|
||||||
|
}: {
|
||||||
|
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||||
|
} = {}) {
|
||||||
|
const query = getSupabaseServerAdminClient()
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_orders')
|
.from('analysis_orders')
|
||||||
.select('*')
|
.select('*')
|
||||||
if (orderStatus) {
|
if (orderStatus) {
|
||||||
query.eq('status', orderStatus);
|
query.eq('status', orderStatus);
|
||||||
}
|
}
|
||||||
const orders = await query.throwOnError();
|
const orders = await query.order('created_at', { ascending: false }).throwOnError();
|
||||||
return orders.data;
|
return orders.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
|||||||
SaadetisId: string;
|
SaadetisId: string;
|
||||||
Email: string;
|
Email: string;
|
||||||
};
|
};
|
||||||
Vastus: {
|
Vastus?: {
|
||||||
ValisTellimuseId: string;
|
ValisTellimuseId: string;
|
||||||
Asutus: {
|
Asutus: {
|
||||||
'@_tyyp': string; // TEOSTAJA
|
'@_tyyp': string; // TEOSTAJA
|
||||||
@@ -246,6 +246,9 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
|||||||
TellimuseOlek: keyof typeof AnalysisOrderStatus;
|
TellimuseOlek: keyof typeof AnalysisOrderStatus;
|
||||||
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
|
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
|
||||||
};
|
};
|
||||||
|
Tellimus?: {
|
||||||
|
ValisTellimuseId: string;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,7 +261,7 @@ export const AnalysisOrderStatus = {
|
|||||||
6: 'CANCELLED',
|
6: 'CANCELLED',
|
||||||
} as const;
|
} as const;
|
||||||
export const NormStatus: Record<number, string> = {
|
export const NormStatus: Record<number, string> = {
|
||||||
1: 'NORMAL',
|
0: 'NORMAL',
|
||||||
2: 'WARNING',
|
1: 'WARNING',
|
||||||
3: 'REQUIRES_ATTENTION',
|
2: 'REQUIRES_ATTENTION',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -18,3 +18,12 @@ export function toTitleCase(str?: string) {
|
|||||||
text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
|
text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortByDate<T>(a: T[] | undefined, key: keyof T): T[] | undefined {
|
||||||
|
return a?.sort((a, b) => {
|
||||||
|
if (!a[key] || !b[key]) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return new Date(b[key] as string).getTime() - new Date(a[key] as string).getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"subject": "Your Synlab order has been placed - {{analysisPackageName}}",
|
"subject": "Your Medreport order has been placed - {{analysisPackageName}}",
|
||||||
"previewText": "Your Synlab order has been placed - {{analysisPackageName}}",
|
"previewText": "Your Medreport order has been placed - {{analysisPackageName}}",
|
||||||
"heading": "Your Synlab order has been placed - {{analysisPackageName}}",
|
"heading": "Your Medreport order has been placed - {{analysisPackageName}}",
|
||||||
"hello": "Hello {{personName}},",
|
"hello": "Hello {{personName}},",
|
||||||
"lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: SYNLAB - {{partnerLocationName}}",
|
"lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: Synlab - {{partnerLocationName}}",
|
||||||
"lines2": "<i>If you are unable to go to the lab to collect the sample, you can go to any other suitable collection point - <a href=\"https://medreport.ee/et/verevotupunktid\">view locations and opening hours</a>.</i>",
|
"lines2": "<i>If you are unable to go to the lab to collect the sample, you can go to any other suitable collection point - <a href=\"https://medreport.ee/et/verevotupunktid\">view locations and opening hours</a>.</i>",
|
||||||
"lines3": "It is recommended to collect the sample in the morning (before 12:00) and not to eat or drink (water can be drunk).",
|
"lines3": "It is recommended to collect the sample in the morning (before 12:00) and not to eat or drink (water can be drunk).",
|
||||||
"lines4": "At the collection point, select the order from the queue: the order from the doctor.",
|
"lines4": "At the collection point, select the order from the queue: the order from the doctor.",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"subject": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
"subject": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||||
"previewText": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
"previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||||
"heading": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
"heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||||
"hello": "Tere {{personName}},",
|
"hello": "Tere {{personName}},",
|
||||||
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: SYNLAB - {{partnerLocationName}}",
|
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Synlab - {{partnerLocationName}}",
|
||||||
"lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>",
|
"lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>",
|
||||||
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
||||||
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
|
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { cn } from '@kit/ui/utils';
|
|||||||
import { toTitleCase } from '~/lib/utils';
|
import { toTitleCase } from '~/lib/utils';
|
||||||
|
|
||||||
import { usePersonalAccountData } from '../hooks/use-personal-account-data';
|
import { usePersonalAccountData } from '../hooks/use-personal-account-data';
|
||||||
|
import { ApplicationRole, ApplicationRoleEnum } from '../types/accounts';
|
||||||
|
|
||||||
const PERSONAL_ACCOUNT_SLUG = 'personal';
|
const PERSONAL_ACCOUNT_SLUG = 'personal';
|
||||||
|
|
||||||
@@ -51,13 +52,13 @@ export function PersonalAccountDropdown({
|
|||||||
id: string | null;
|
id: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
picture_url: string | null;
|
picture_url: string | null;
|
||||||
application_role: string;
|
application_role: ApplicationRole | null;
|
||||||
};
|
};
|
||||||
accounts: {
|
accounts: {
|
||||||
label: string | null;
|
label: string | null;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
application_role: string;
|
application_role: ApplicationRole | null;
|
||||||
}[];
|
}[];
|
||||||
signOutRequested: () => unknown;
|
signOutRequested: () => unknown;
|
||||||
|
|
||||||
@@ -97,13 +98,14 @@ export function PersonalAccountDropdown({
|
|||||||
|
|
||||||
const isSuperAdmin = useMemo(() => {
|
const isSuperAdmin = useMemo(() => {
|
||||||
const hasAdminRole =
|
const hasAdminRole =
|
||||||
personalAccountData?.application_role === 'super_admin';
|
personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
|
||||||
|
|
||||||
return hasAdminRole && hasTotpFactor;
|
return hasAdminRole && hasTotpFactor;
|
||||||
}, [user, personalAccountData, hasTotpFactor]);
|
}, [user, personalAccountData, hasTotpFactor]);
|
||||||
|
|
||||||
const isDoctor = useMemo(() => {
|
const isDoctor = useMemo(() => {
|
||||||
const hasDoctorRole = personalAccountData?.application_role === 'doctor';
|
const hasDoctorRole =
|
||||||
|
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
|
||||||
|
|
||||||
return hasDoctorRole && hasTotpFactor;
|
return hasDoctorRole && hasTotpFactor;
|
||||||
}, [user, personalAccountData, hasTotpFactor]);
|
}, [user, personalAccountData, hasTotpFactor]);
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
export type UserAnalysisElement = Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
export type UserAnalysisElement =
|
||||||
export type UserAnalysisResponse = Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||||
elements: UserAnalysisElement[];
|
export type UserAnalysisResponse =
|
||||||
};
|
Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
||||||
|
elements: UserAnalysisElement[];
|
||||||
|
};
|
||||||
export type UserAnalysis = UserAnalysisResponse[];
|
export type UserAnalysis = UserAnalysisResponse[];
|
||||||
|
|
||||||
|
export type ApplicationRole =
|
||||||
|
Database['medreport']['Tables']['accounts']['Row']['application_role'];
|
||||||
|
export enum ApplicationRoleEnum {
|
||||||
|
User = 'user',
|
||||||
|
Doctor = 'doctor',
|
||||||
|
SuperAdmin = 'super_admin',
|
||||||
|
}
|
||||||
@@ -114,7 +114,8 @@ export class TeamAccountsApi {
|
|||||||
role,
|
role,
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
picture_url
|
picture_url,
|
||||||
|
application_role
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1563,6 +1563,9 @@ export type Database = {
|
|||||||
Views: {
|
Views: {
|
||||||
user_account_workspace: {
|
user_account_workspace: {
|
||||||
Row: {
|
Row: {
|
||||||
|
application_role:
|
||||||
|
| Database["medreport"]["Enums"]["application_role"]
|
||||||
|
| null
|
||||||
id: string | null
|
id: string | null
|
||||||
name: string | null
|
name: string | null
|
||||||
picture_url: string | null
|
picture_url: string | null
|
||||||
@@ -1574,6 +1577,9 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
user_accounts: {
|
user_accounts: {
|
||||||
Row: {
|
Row: {
|
||||||
|
application_role:
|
||||||
|
| Database["medreport"]["Enums"]["application_role"]
|
||||||
|
| null
|
||||||
id: string | null
|
id: string | null
|
||||||
name: string | null
|
name: string | null
|
||||||
picture_url: string | null
|
picture_url: string | null
|
||||||
@@ -1634,7 +1640,9 @@ export type Database = {
|
|||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
create_team_account: {
|
create_team_account: {
|
||||||
Args: { account_name: string; new_personal_code: string }
|
Args:
|
||||||
|
| { account_name: string }
|
||||||
|
| { account_name: string; new_personal_code: string }
|
||||||
Returns: {
|
Returns: {
|
||||||
application_role: Database["medreport"]["Enums"]["application_role"]
|
application_role: Database["medreport"]["Enums"]["application_role"]
|
||||||
city: string | null
|
city: string | null
|
||||||
@@ -1801,6 +1809,7 @@ export type Database = {
|
|||||||
primary_owner_user_id: string
|
primary_owner_user_id: string
|
||||||
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
|
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
|
||||||
permissions: Database["medreport"]["Enums"]["app_permissions"][]
|
permissions: Database["medreport"]["Enums"]["app_permissions"][]
|
||||||
|
application_role: Database["medreport"]["Enums"]["application_role"]
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
transfer_team_account_ownership: {
|
transfer_team_account_ownership: {
|
||||||
@@ -1819,6 +1828,22 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
}
|
}
|
||||||
|
update_analysis_order_status: {
|
||||||
|
Args: {
|
||||||
|
order_id: number
|
||||||
|
medusa_order_id_param: string
|
||||||
|
status_param: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||||
|
}
|
||||||
|
Returns: {
|
||||||
|
analysis_element_ids: number[] | null
|
||||||
|
analysis_ids: number[] | null
|
||||||
|
created_at: string
|
||||||
|
id: number
|
||||||
|
medusa_order_id: string
|
||||||
|
status: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
upsert_order: {
|
upsert_order: {
|
||||||
Args: {
|
Args: {
|
||||||
target_account_id: string
|
target_account_id: string
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
"orderNewAnalysis": "Order new analyses",
|
"orderNewAnalysis": "Order new analyses",
|
||||||
"waitingForResults": "Waiting for results",
|
"waitingForResults": "Waiting for results",
|
||||||
"noAnalysisElements": "No analysis orders found",
|
"noAnalysisElements": "No analysis orders found",
|
||||||
|
"noAnalysisOrders": "No analysis orders found",
|
||||||
"analysisDate": "Analysis result date",
|
"analysisDate": "Analysis result date",
|
||||||
"results": {
|
"results": {
|
||||||
"range": {
|
"range": {
|
||||||
"normal": "Normal range"
|
"normal": "Normal range"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"orderTitle": "Order number {{orderNumber}}"
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,11 @@
|
|||||||
"cholesterol": "Cholesterol",
|
"cholesterol": "Cholesterol",
|
||||||
"ldlCholesterol": "LDL Cholesterol",
|
"ldlCholesterol": "LDL Cholesterol",
|
||||||
"smoking": "Smoking",
|
"smoking": "Smoking",
|
||||||
"recommendedForYou": "Recommended for you"
|
"recommendedForYou": "Recommended for you",
|
||||||
|
"heroCard": {
|
||||||
|
"orderAnalysis": {
|
||||||
|
"title": "Order analysis",
|
||||||
|
"description": "Select an analysis to get started"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "Select analysis",
|
"title": "Select analysis",
|
||||||
"description": "Select the analysis that suits your needs"
|
"description": "Select the analysis that suits your needs",
|
||||||
|
"analysisNotAvailable": "Analysis is not available currently"
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,12 @@
|
|||||||
"orderNewAnalysis": "Telli uued analüüsid",
|
"orderNewAnalysis": "Telli uued analüüsid",
|
||||||
"waitingForResults": "Tulemuse ootel",
|
"waitingForResults": "Tulemuse ootel",
|
||||||
"noAnalysisElements": "Veel ei ole tellitud analüüse",
|
"noAnalysisElements": "Veel ei ole tellitud analüüse",
|
||||||
|
"noAnalysisOrders": "Veel ei ole analüüside tellimusi",
|
||||||
"analysisDate": "Analüüsi vastuse kuupäev",
|
"analysisDate": "Analüüsi vastuse kuupäev",
|
||||||
"results": {
|
"results": {
|
||||||
"range": {
|
"range": {
|
||||||
"normal": "Normaalne vahemik"
|
"normal": "Normaalne vahemik"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"orderTitle": "Tellimus {{orderNumber}}"
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,11 @@
|
|||||||
"cholesterol": "Kolesterool",
|
"cholesterol": "Kolesterool",
|
||||||
"ldlCholesterol": "LDL kolesterool",
|
"ldlCholesterol": "LDL kolesterool",
|
||||||
"smoking": "Suitsetamine",
|
"smoking": "Suitsetamine",
|
||||||
"recommendedForYou": "Soovitused sulle"
|
"recommendedForYou": "Soovitused sulle",
|
||||||
|
"heroCard": {
|
||||||
|
"orderAnalysis": {
|
||||||
|
"title": "Telli analüüs",
|
||||||
|
"description": "Telli endale sobiv analüüs"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "Vali analüüs",
|
"title": "Vali analüüs",
|
||||||
"description": "Vali enda vajadustele sobiv analüüs"
|
"description": "Vali enda vajadustele sobiv analüüs",
|
||||||
|
"analysisNotAvailable": "Analüüsi tellimine ei ole hetkel saadaval"
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ function send_medipost_test_response() {
|
|||||||
curl -X POST "$HOSTNAME/api/order/medipost-test-response" \
|
curl -X POST "$HOSTNAME/api/order/medipost-test-response" \
|
||||||
--header "x-jobs-api-key: $JOBS_API_TOKEN" \
|
--header "x-jobs-api-key: $JOBS_API_TOKEN" \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'" }'
|
--data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'", "maxItems": 2 }'
|
||||||
}
|
}
|
||||||
|
|
||||||
function sync_analysis_results() {
|
function sync_analysis_results() {
|
||||||
|
|||||||
19
schedule-setup/setup_send_analysis_test_results_cron.sql
Normal file
19
schedule-setup/setup_send_analysis_test_results_cron.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- Enable required extensions for cron jobs and HTTP requests
|
||||||
|
create extension if not exists pg_cron;
|
||||||
|
create extension if not exists pg_net;
|
||||||
|
|
||||||
|
-- Schedule the test-medipost-responses job to run every 15 minutes
|
||||||
|
select
|
||||||
|
cron.schedule(
|
||||||
|
'send-test-medipost-responses-every-15-minutes', -- Unique job name
|
||||||
|
'*/15 * * * *', -- Cron schedule: every 15 minutes
|
||||||
|
$$
|
||||||
|
select
|
||||||
|
net.http_post(
|
||||||
|
url := 'https://test.medreport.ee/api/job/test-medipost-responses',
|
||||||
|
headers := jsonb_build_object(
|
||||||
|
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
|
||||||
|
)
|
||||||
|
) as request_id;
|
||||||
|
$$
|
||||||
|
);
|
||||||
19
schedule-setup/setup_sync_analysis_results_cron.sql
Normal file
19
schedule-setup/setup_sync_analysis_results_cron.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- Enable required extensions for cron jobs and HTTP requests
|
||||||
|
create extension if not exists pg_cron;
|
||||||
|
create extension if not exists pg_net;
|
||||||
|
|
||||||
|
-- Schedule the sync-analysis-results job to run every 15 minutes
|
||||||
|
select
|
||||||
|
cron.schedule(
|
||||||
|
'sync-analysis-results-every-15-minutes', -- Unique job name
|
||||||
|
'*/15 * * * *', -- Cron schedule: every 15 minutes
|
||||||
|
$$
|
||||||
|
select
|
||||||
|
net.http_post(
|
||||||
|
url := 'https://test.medreport.ee/api/job/sync-analysis-results',
|
||||||
|
headers := jsonb_build_object(
|
||||||
|
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
|
||||||
|
)
|
||||||
|
) as request_id;
|
||||||
|
$$
|
||||||
|
);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- Function "medreport.update_analysis_order_status"
|
||||||
|
-- Update an analysis order status
|
||||||
|
create
|
||||||
|
or replace function medreport.update_analysis_order_status (
|
||||||
|
order_id bigint,
|
||||||
|
medusa_order_id_param text,
|
||||||
|
status_param medreport.analysis_order_status
|
||||||
|
) returns medreport.analysis_orders
|
||||||
|
set
|
||||||
|
search_path = '' as $$
|
||||||
|
declare
|
||||||
|
updated_order medreport.analysis_orders;
|
||||||
|
begin
|
||||||
|
update medreport.analysis_orders
|
||||||
|
set status = status_param
|
||||||
|
where (id = order_id OR medusa_order_id = medusa_order_id_param)
|
||||||
|
returning * into updated_order;
|
||||||
|
|
||||||
|
return updated_order;
|
||||||
|
|
||||||
|
end;
|
||||||
|
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
grant
|
||||||
|
execute on function medreport.update_analysis_order_status (
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
medreport.analysis_order_status
|
||||||
|
) to service_role;
|
||||||
|
|
||||||
|
-- example:
|
||||||
|
-- select medreport.update_analysis_order_status(-1, 'order_01K1TQQHZGPXKDHAH81TDSNGXR', 'CANCELLED')
|
||||||
76
supabase/migrations/20250814071257_update_accounts_view.sql
Normal file
76
supabase/migrations/20250814071257_update_accounts_view.sql
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
CREATE OR REPLACE VIEW medreport.user_accounts AS
|
||||||
|
SELECT
|
||||||
|
account.id,
|
||||||
|
account.name,
|
||||||
|
account.picture_url,
|
||||||
|
account.slug,
|
||||||
|
membership.account_role AS role,
|
||||||
|
COALESCE(account.application_role, 'user') AS application_role
|
||||||
|
FROM medreport.accounts account
|
||||||
|
JOIN medreport.accounts_memberships membership ON (account.id = membership.account_id)
|
||||||
|
WHERE (
|
||||||
|
membership.user_id = (SELECT auth.uid())
|
||||||
|
AND account.is_personal_account = false
|
||||||
|
AND account.id IN (
|
||||||
|
SELECT accounts_memberships.account_id
|
||||||
|
FROM medreport.accounts_memberships
|
||||||
|
WHERE accounts_memberships.user_id = (SELECT auth.uid())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT SELECT ON medreport.user_accounts TO authenticated, service_role;
|
||||||
|
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS medreport.team_account_workspace(text);
|
||||||
|
|
||||||
|
CREATE FUNCTION medreport.team_account_workspace(account_slug text)
|
||||||
|
RETURNS TABLE(id uuid, name character varying, picture_url character varying, slug text, role character varying, role_hierarchy_level integer, primary_owner_user_id uuid, subscription_status medreport.subscription_status, permissions medreport.app_permissions[], application_role medreport.application_role)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SET search_path TO ''
|
||||||
|
AS $function$begin
|
||||||
|
return QUERY
|
||||||
|
select
|
||||||
|
accounts.id,
|
||||||
|
accounts.name,
|
||||||
|
accounts.picture_url,
|
||||||
|
accounts.slug,
|
||||||
|
accounts.application_role,
|
||||||
|
accounts_memberships.account_role,
|
||||||
|
roles.hierarchy_level,
|
||||||
|
accounts.primary_owner_user_id,
|
||||||
|
subscriptions.status,
|
||||||
|
array_agg(role_permissions.permission)
|
||||||
|
from
|
||||||
|
medreport.accounts
|
||||||
|
join medreport.accounts_memberships on accounts.id = accounts_memberships.account_id
|
||||||
|
left join medreport.subscriptions on accounts.id = subscriptions.account_id
|
||||||
|
join medreport.roles on accounts_memberships.account_role = roles.name
|
||||||
|
left join medreport.role_permissions on accounts_memberships.account_role = role_permissions.role
|
||||||
|
where
|
||||||
|
accounts.slug = account_slug
|
||||||
|
and medreport.accounts_memberships.user_id = (select auth.uid())
|
||||||
|
group by
|
||||||
|
accounts.id,
|
||||||
|
accounts_memberships.account_role,
|
||||||
|
subscriptions.status,
|
||||||
|
roles.hierarchy_level;
|
||||||
|
end;$function$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION medreport.team_account_workspace(text) TO authenticated, service_role;
|
||||||
|
|
||||||
|
create or replace view medreport.user_account_workspace as SELECT accounts.id,
|
||||||
|
accounts.name,
|
||||||
|
accounts.picture_url,
|
||||||
|
( SELECT subscriptions.status
|
||||||
|
FROM medreport.subscriptions
|
||||||
|
WHERE (subscriptions.account_id = accounts.id)
|
||||||
|
LIMIT 1) AS subscription_status,
|
||||||
|
accounts.application_role
|
||||||
|
FROM medreport.accounts
|
||||||
|
WHERE ((accounts.primary_owner_user_id = ( SELECT auth.uid() AS uid)) AND (accounts.is_personal_account = true))
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
grant
|
||||||
|
select
|
||||||
|
on medreport.user_account_workspace to authenticated,
|
||||||
|
service_role;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Increase the body column limit for notifications to support larger email content (up to 128KB)
|
||||||
|
ALTER TABLE medreport.notifications ALTER COLUMN body TYPE VARCHAR(131072);
|
||||||
Reference in New Issue
Block a user