10 Commits

Author SHA1 Message Date
ab92c3b4d0 test3 2025-09-08 00:47:04 +03:00
e324872c3c test2 2025-09-08 00:46:45 +03:00
a44f9c9207 test 2025-09-08 00:45:43 +03:00
0cf04b4f55 wip 2025-09-05 01:39:06 +03:00
84216c3ced test 2025-09-04 13:40:21 +03:00
0037241558 allow transferCart to fail on register 2025-09-04 13:36:46 +03:00
c7f89723e3 allow transferCart to fail on login/register 2025-09-04 13:19:09 +03:00
4a06059a25 medusa product can have either analysiselement or analysis originalId 2025-09-04 13:18:42 +03:00
a0abb44257 hide dashboard recommendations block 2025-09-04 12:57:18 +03:00
283b502963 fix tooltip should wrap long text 2025-09-04 12:41:18 +03:00
43 changed files with 52573 additions and 300 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
.git
Dockerfile

View File

@@ -26,14 +26,65 @@ EMAIL_PORT=1025 # or 465 for SSL
EMAIL_TLS=false
NODE_TLS_REJECT_UNAUTHORIZED=0
# MEDIPOST
MEDIPOST_URL=https://meditest.medisoft.ee:7443/Medipost/MedipostServlet
MEDIPOST_USER=trvurgtst
MEDIPOST_PASSWORD=SRB48HZMV
MEDIPOST_RECIPIENT=trvurgtst
MEDIPOST_MESSAGE_SENDER=trvurgtst
#MEDIPOST_URL=https://medipost2.medisoft.ee:8443/Medipost/MedipostServlet
#MEDIPOST_USER=medreport
#MEDIPOST_PASSWORD=85MXFFDB7
#MEDIPOST_RECIPIENT=HTI
#MEDIPOST_MESSAGE_SENDER=medreport
# MEDUSA
MEDUSA_BACKEND_URL=http://localhost:9000
MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000
MEDUSA_SECRET_API_KEY=sk_b332d525212ab4078ef73fb2b8232c3beebccc4a460e2c7abf6e187a458d60cf
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_e23a820689a07d55aa0a0ad187268559f5d6288ecb0768ff4520516285bdef84
#MEDUSA_BACKEND_URL=https://backoffice-test.medreport.ee
#MEDUSA_BACKEND_PUBLIC_URL=https://backoffice-test.medreport.ee
#MEDUSA_SECRET_API_KEY=sk_fdb1808fbabf62979cc46316aa997378ffbb87882883e8f5c3ee47cee39dcac5
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_827a2ab863021cb67993f1d81078f81bfce4b4e0da642d8c0f5398ded9d8fd32
#MEDUSA_BACKEND_URL=https://backoffice.medreport.ee
#MEDUSA_BACKEND_PUBLIC_URL=https://backoffice.medreport.ee
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_068d930c33fea53608a410d84a51935f6ce2ccec5bef8e0ecf75eaee602ac486
#MEDUSA_SECRET_API_KEY=sk_fdb1808fbabf62979cc46316aa997378ffbb87882883e8f5c3ee47cee39dcac5
#MEDUSA_BACKEND_URL=http://5.181.51.38:9000
#MEDUSA_BACKEND_PUBLIC_URL=http://5.181.51.38:9000
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_0ec86252438b38ce18d5601f7877e4395d7e0a6afa8687dfea8d37af33015633
# MONTONIO
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
MONTONIO_API_URL=https://sandbox-stargate.montonio.com
#NEXT_PUBLIC_MONTONIO_ACCESS_KEY=13e3686a-e7ad-41f6-998b-3f7d7de17654
#MONTONIO_SECRET_KEY=wTd4BZ01h80KZLMPL4mjt0RCFxKaYRSu9mMB1PQZCxnw
#MONTONIO_API_URL=https://stargate.montonio.com
# JOBS
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
# SUPABASE
NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1Njk4NDYyOCwiZXhwIjoyMDcyNTYwNjI4fQ.1UZR7AqSD9bOy1gtZRGhOCNoESsw2W-DoFDDsNNMwoE
#NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
#NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
#SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0NjUyODEyMywiZXhwIjoyMDYyMTA0MTIzfQ.KVcnkZ21Pd0XkJho23dZqFHawVTLQqfvF7l2RxsELLk
#NEXT_PUBLIC_SUPABASE_URL=http://5.181.51.38:54321
#NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
#SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
### TEST.MEDREPORT.ee ###
DB_PASSWORD=T#u-$M7%RjbA@L@

View File

@@ -6,10 +6,10 @@
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
# SUPABASE
# NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
# # MONTONIO
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
# MONTONIO
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead

View File

@@ -6,10 +6,10 @@
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
# SUPABASE
# NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
NEXT_PUBLIC_SUPABASE_URL=https://kaldvociniytdbbcxvqk.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImthbGR2b2Npbml5dGRiYmN4dnFrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTYzNjQ5OTYsImV4cCI6MjA3MTk0MDk5Nn0.eixihH2KGkJZolY9FiQDicJOo2kxvXrSe6gGUCrkLo0
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
# # MONTONIO
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
# MONTONIO
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead

View File

@@ -22,12 +22,12 @@ COPY . .
ENV NODE_ENV=production
RUN set -a \
&& . .env \
&& . .env.production \
&& . .env.staging \
&& set +a \
&& node check-env.js \
&& pnpm build
&& . .env \
&& . .env.production \
&& . .env.staging \
&& set +a \
&& node check-env.js \
&& pnpm build
# --- Stage 2: Runtime ---
@@ -41,13 +41,13 @@ COPY --from=builder /app ./
RUN cp ".env.${APP_ENV}" .env.local
RUN npm install -g pnpm@9 \
&& pnpm install --prod --frozen-lockfile
&& pnpm install --prod --frozen-lockfile
ENV NODE_ENV=production
# 🔍 Optional: Log key envs for debug
RUN echo "📄 .env contents:" && cat .env.local \
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
EXPOSE 3000

View File

@@ -0,0 +1,21 @@
import { NextResponse } from "next/server";
import { sendEmail } from "~/lib/services/mailer.service";
export const GET = async () => {
const { renderInviteEmail } = await import('@kit/email-templates');
const html = await renderInviteEmail({
language: 'en',
teamName: 'Test Team',
invitedUserEmail: 'test@example.com',
productName: 'Test Product',
teamLogo: 'https://placehold.co/100x100',
inviter: 'John Doe',
link: 'https://www.google.com',
});
return NextResponse.json({
html,
length: html.html.length,
});
};

View File

@@ -2,10 +2,10 @@ import axios from 'axios';
import { XMLParser } from 'fast-xml-parser';
import fs from 'fs';
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost.types';
import { IMedipostPublicMessageDataParsed, IUuringElement } from '~/lib/services/medipost.types';
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service';
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
import { createAnalysisElement } from '~/lib/services/analysis-element.service';
import { createAnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
import { createCodes } from '~/lib/services/codes.service';
import { getLatestPublicMessageListItem } from '~/lib/services/medipost.service';
import type { ICode } from '~/lib/types/code';
@@ -80,81 +80,92 @@ export default async function syncAnalysisGroups() {
}
const codes: ICode[] = [];
const analysesToCreate: { analysisGroupId: number, analyses: IUuringElement[], analysisElementId: number }[] = [];
for (const analysisGroup of analysisGroups) {
let analysisGroupId: number | undefined;
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
if (existingAnalysisGroup) {
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`);
continue;
analysisGroupId = existingAnalysisGroup.id;
} else {
// SAVE ANALYSIS GROUP
analysisGroupId = await createAnalysisGroup({
id: analysisGroup.UuringuGruppId,
name: analysisGroup.UuringuGruppNimi,
order: analysisGroup.UuringuGruppJarjekord,
});
const analysisGroupCodes = toArray(analysisGroup.Kood);
codes.push(
...analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: analysisGroupId!,
analysis_element_id: null,
analysis_id: null,
})),
);
}
// SAVE ANALYSIS GROUP
const analysisGroupId = await createAnalysisGroup({
id: analysisGroup.UuringuGruppId,
name: analysisGroup.UuringuGruppNimi,
order: analysisGroup.UuringuGruppJarjekord,
});
const analysisGroupCodes = toArray(analysisGroup.Kood);
codes.push(
...analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: analysisGroupId,
analysis_element_id: null,
analysis_id: null,
})),
);
const analysisGroupItems = toArray(analysisGroup.Uuring);
for (const item of analysisGroupItems) {
const analysisElement = item.UuringuElement;
const insertedAnalysisElementId = await createAnalysisElement({
analysisElement,
analysisGroupId,
materialGroups: toArray(item.MaterjalideGrupp),
});
let insertedAnalysisElementId: number | undefined;
const existingAnalysisElement = (await getAnalysisElements({ originalIds: [analysisElement.UuringId] }))?.[0];
if (existingAnalysisElement) {
console.info(`Analysis element '${analysisElement.UuringNimi}' already exists`);
insertedAnalysisElementId = existingAnalysisElement.id;
} else {
insertedAnalysisElementId = await createAnalysisElement({
analysisElement,
analysisGroupId: analysisGroupId!,
materialGroups: toArray(item.MaterjalideGrupp),
});
if (analysisElement.Kood) {
const analysisElementCodes = toArray(analysisElement.Kood);
codes.push(
...analysisElementCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: null,
analysis_element_id: insertedAnalysisElementId!,
analysis_id: null,
})),
);
}
}
const analyses = toArray(analysisElement.UuringuElement);
if (analyses?.length && insertedAnalysisElementId) {
analysesToCreate.push({ analysisGroupId: analysisGroupId!, analyses, analysisElementId: insertedAnalysisElementId });
}
}
}
for (const { analysisGroupId, analyses, analysisElementId } of analysesToCreate) {
for (const analysis of analyses) {
const insertedAnalysisId = await createAnalysis(analysis, analysisElementId);
if (analysis.Kood) {
const analysisCodes = toArray(analysis.Kood);
if (analysisElement.Kood) {
const analysisElementCodes = toArray(analysisElement.Kood);
codes.push(
...analysisElementCodes.map((kood) => ({
...analysisCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: null,
analysis_element_id: insertedAnalysisElementId,
analysis_id: null,
analysis_element_id: null,
analysis_id: insertedAnalysisId,
})),
);
}
const analyses = analysisElement.UuringuElement;
if (analyses?.length) {
for (const analysis of analyses) {
const insertedAnalysisId = await createAnalysis(analysis, analysisGroupId);
if (analysis.Kood) {
const analysisCodes = toArray(analysis.Kood);
codes.push(
...analysisCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: null,
analysis_element_id: null,
analysis_id: insertedAnalysisId,
})),
);
}
}
}
}
}

View File

@@ -22,6 +22,7 @@ export const POST = async (request: NextRequest) => {
console.error("Error syncing analysis groups", e);
return NextResponse.json({
message: 'Failed to sync analysis groups',
error: e instanceof Error ? JSON.stringify(e, undefined, 2) : 'Unknown error',
}, { status: 500 });
}
};

View File

@@ -1,7 +1,6 @@
"use client"
import { Badge, Text } from "@medusajs/ui"
import { toast } from '@kit/ui/sonner';
import { Badge, Heading, Text } from "@medusajs/ui"
import React, { useActionState } from "react";
import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
@@ -32,19 +31,11 @@ export default function DiscountCode({ cart }: {
const removePromotionCode = async (code: string) => {
const validPromotions = promotions.filter(
(promotion) => promotion.code !== code,
(promotion) => promotion.code !== code
)
await applyPromotions(
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!),
{
onSuccess: () => {
toast.success(t('cart:discountCode.removeSuccess'));
},
onError: () => {
toast.error(t('cart:discountCode.removeError'));
},
}
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!)
)
}
@@ -54,14 +45,7 @@ export default function DiscountCode({ cart }: {
.map((p) => p.code!)
codes.push(code.toString())
await applyPromotions(codes, {
onSuccess: () => {
toast.success(t('cart:discountCode.addSuccess'));
},
onError: () => {
toast.error(t('cart:discountCode.addError'));
},
});
await applyPromotions(codes)
form.reset()
}
@@ -80,7 +64,7 @@ export default function DiscountCode({ cart }: {
<Form {...form}>
<form
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2"
className="w-full mb-2 flex gap-x-2"
>
<FormField
name={'code'}
@@ -103,12 +87,16 @@ export default function DiscountCode({ cart }: {
</form>
</Form>
{promotions.length > 0 ? (
<div className="w-full flex items-center mt-4">
<div className="flex flex-col w-full gap-y-2">
<p>
<Trans i18nKey={'cart:discountCode.appliedCodes'} />
</p>
<p className="text-sm text-muted-foreground">
<Trans i18nKey={'cart:discountCode.subtitle'} />
</p>
{promotions.length > 0 && (
<div className="w-full flex items-center">
<div className="flex flex-col w-full">
<Heading className="txt-medium mb-2">
Promotion(s) applied:
</Heading>
{promotions.map((promotion) => {
return (
@@ -122,7 +110,6 @@ export default function DiscountCode({ cart }: {
<Badge
color={promotion.is_automatic ? "green" : "grey"}
size="small"
className="px-4"
>
{promotion.code}
</Badge>{" "}
@@ -164,7 +151,7 @@ export default function DiscountCode({ cart }: {
>
<Trash size={14} />
<span className="sr-only">
<Trans i18nKey={'cart:discountCode.remove'} />
Remove discount code from order
</span>
</button>
)}
@@ -173,10 +160,6 @@ export default function DiscountCode({ cart }: {
})}
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">
<Trans i18nKey={'cart:discountCode.subtitle'} />
</p>
)}
</div>
)

View File

@@ -17,8 +17,9 @@ import { formatCurrency } from "@/packages/shared/src/utils";
import { useTranslation } from "react-i18next";
import { handleNavigateToPayment } from "@/lib/services/medusaCart.service";
import AnalysisLocation from "./analysis-location";
import { composeOrderXML, getOrderedAnalysisIds } from "~/lib/services/medipost.service";
const IS_DISCOUNT_SHOWN = true as boolean;
const IS_DISCOUNT_SHOWN = false as boolean;
export default function Cart({
cart,
@@ -69,7 +70,7 @@ export default function Cart({
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
const isLocationsShown = synlabAnalyses.length > 0;
return (
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4">
<div className="flex flex-col bg-white gap-y-6">
@@ -77,62 +78,28 @@ export default function Cart({
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
</div>
{hasCartItems && (
<>
<div className="flex justify-end gap-x-4 px-6 pt-4">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm text-muted-foreground">
<Trans i18nKey="cart:subtotal" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.subtotal,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
<div className="flex justify-end gap-x-4 px-6 py-4">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm">
<Trans i18nKey="cart:total" />
</p>
</div>
<div className="flex justify-end gap-x-4 px-6 py-2">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm text-muted-foreground">
<Trans i18nKey="cart:promotionsTotal" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.discount_total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
<div className="flex justify-end gap-x-4 px-6">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm">
<Trans i18nKey="cart:total" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div>
</>
</div>
)}
<div className="flex sm:flex-row flex-col gap-y-6 py-8 gap-x-4">
<div className="flex gap-y-6 py-8">
{IS_DISCOUNT_SHOWN && (
<Card
className="flex flex-col justify-between w-full sm:w-1/2"
className="flex flex-col justify-between w-1/2"
>
<CardHeader className="pb-4">
<h5>
@@ -147,7 +114,7 @@ export default function Cart({
{isLocationsShown && (
<Card
className="flex flex-col justify-between w-full sm:w-1/2"
className="flex flex-col justify-between w-1/2"
>
<CardHeader className="pb-4">
<h5>
@@ -167,6 +134,26 @@ export default function Cart({
<Trans i18nKey="cart:checkout.goToCheckout" />
</Button>
</div>
<Button type='button' onClick={async () => {
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder: { items: cart.items ?? [] } });
const xml = await composeOrderXML({
person: {
idCode: '1234567890',
firstName: 'John',
lastName: 'Doe',
phone: '1234567890',
},
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderId: '1234567890',
orderCreatedAt: new Date(),
});
console.log('test', { items: cart.items, ids: orderedAnalysisElementsIds, xml });
console.log('test', xml);
}}>
Test
</Button>
</div>
);
}

8362
current-test-data.sql Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@ import type { Tables } from '@/packages/supabase/src/database.types';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import type { IUuringElement } from "./medipost.types";
type AnalysesWithGroupsAndElements = ({
export type AnalysesWithGroupsAndElements = ({
analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
};

View File

@@ -10,7 +10,6 @@ import {
getClientInstitution,
getClientPerson,
getConfidentiality,
getOrderEnteredPerson,
getPais,
getPatient,
getProviderInstitution,
@@ -38,10 +37,10 @@ import { Tables } from '@kit/supabase/database';
import { createAnalysisGroup } from './analysis-group.service';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
import { getOrder, updateOrderStatus } from './order.service';
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
import { getAnalyses } from './analyses.service';
import { AnalysisElement, getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
import { AnalysesWithGroupsAndElements, getAnalyses } from './analyses.service';
import { getAccountAdmin } from './account.service';
import { StoreOrder } from '@medusajs/types';
import { StoreOrder, StoreOrderLineItem } from '@medusajs/types';
import { listProducts } from '@lib/data/products';
import { listRegions } from '@lib/data/regions';
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
@@ -481,72 +480,60 @@ export async function composeOrderXML({
throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`);
}
const uniques = [
...analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ?? [],
...analyses?.flatMap(({ analysis_elements }) => analysis_elements.analysis_groups) ?? []
];
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
uniqBy(
(
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ??
[]
).concat(
analyses?.flatMap(
({ analysis_elements }) => analysis_elements.analysis_groups,
) ?? [],
),
'id',
);
uniqBy(uniques, 'id');
console.log('analysisGroups', { analysisGroups, uniques });
const specimenSection = [];
const analysisSection = [];
let order = 1;
for (const currentGroup of analysisGroups) {
let relatedAnalysisElement = analysisElements?.find(
(element) => element.analysis_groups.id === currentGroup.id,
);
const relatedAnalyses = analyses?.filter((analysis) => {
return analysis.analysis_elements.analysis_groups.id === currentGroup.id;
const relatedAnalysisElements = await getRelatedAnalysisElements({
analysisElements,
analyses,
currentGroup,
});
if (!relatedAnalysisElement) {
relatedAnalysisElement = relatedAnalyses?.find(
(relatedAnalysis) =>
relatedAnalysis.analysis_elements.analysis_groups.id ===
currentGroup.id,
)?.analysis_elements;
}
for (const relatedAnalysisElement of relatedAnalysisElements) {
if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
throw new Error(
`Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`,
);
}
if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
throw new Error(
`Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`,
for (const group of relatedAnalysisElement?.material_groups as MaterjalideGrupp[]) {
const materials = toArray(group.Materjal);
const specimenXml = materials.flatMap(
({ MaterjaliNimi, MaterjaliTyyp, MaterjaliTyypOID, Konteiner }) => {
return toArray(Konteiner).map((container) =>
getSpecimen(
MaterjaliTyypOID,
MaterjaliTyyp,
MaterjaliNimi,
order,
container.ProovinouKoodOID,
container.ProovinouKood,
),
);
},
);
specimenSection.push(...specimenXml);
}
const groupXml = getAnalysisGroup(
currentGroup.original_id,
currentGroup.name,
order,
relatedAnalysisElement,
);
order++;
analysisSection.push(groupXml);
}
for (const group of relatedAnalysisElement?.material_groups as MaterjalideGrupp[]) {
const materials = toArray(group.Materjal);
const specimenXml = materials.flatMap(
({ MaterjaliNimi, MaterjaliTyyp, MaterjaliTyypOID, Konteiner }) => {
return toArray(Konteiner).map((container) =>
getSpecimen(
MaterjaliTyypOID,
MaterjaliTyyp,
MaterjaliNimi,
order,
container.ProovinouKoodOID,
container.ProovinouKood,
),
);
},
);
specimenSection.push(...specimenXml);
}
const groupXml = getAnalysisGroup(
currentGroup.original_id,
currentGroup.name,
order,
relatedAnalysisElement,
);
order++;
analysisSection.push(groupXml);
}
return `<?xml version="1.0" encoding="UTF-8"?>
@@ -554,12 +541,14 @@ export async function composeOrderXML({
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
<Tellimus cito="EI">
<ValisTellimuseId>${orderId}</ValisTellimuseId>
<!--<TellijaAsutus>-->
${getClientInstitution()}
<!--<TeostajaAsutus>-->
${getProviderInstitution()}
${getClientPerson()}
${getOrderEnteredPerson()}
<!--<TellijaIsik>-->
${getClientPerson(person)}
<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
${getPatient(person)}
${getPatient(person)}
${getConfidentiality()}
${specimenSection.join('')}
${analysisSection?.join('')}
@@ -694,7 +683,7 @@ async function syncPrivateMessage({
);
}
const { data: allOrderResponseElements} = await supabase
const { data: allOrderResponseElements } = await supabase
.schema('medreport')
.from('analysis_response_elements')
.select('*')
@@ -783,10 +772,13 @@ export async function sendOrderToMedipost({
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
}
type OrderItems = {
items: Pick<StoreOrderLineItem, 'product'>[];
}
export async function getOrderedAnalysisIds({
medusaOrder,
}: {
medusaOrder: StoreOrder;
medusaOrder: OrderItems;
}): Promise<{
analysisElementId?: number;
analysisId?: number;
@@ -794,7 +786,7 @@ export async function getOrderedAnalysisIds({
const countryCodes = await listRegions();
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
async function getOrderedAnalysisElements(medusaOrder: StoreOrder) {
async function getOrderedAnalysisElements(medusaOrder: OrderItems) {
const originalIds = (medusaOrder?.items ?? [])
.map((a) => a.product?.metadata?.analysisIdOriginal)
.filter((a) => typeof a === 'string') as string[];
@@ -802,7 +794,7 @@ export async function getOrderedAnalysisIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
}
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
async function getOrderedAnalyses(medusaOrder: OrderItems) {
const originalIds = (medusaOrder?.items ?? [])
.map((a) => a.product?.metadata?.analysisIdOriginal)
.filter((a) => typeof a === 'string') as string[];
@@ -810,7 +802,7 @@ export async function getOrderedAnalysisIds({
return analyses.map(({ id }) => ({ analysisId: id }));
}
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
async function getOrderedAnalysisPackages(medusaOrder: OrderItems) {
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
if (orderedPackageIds.length === 0) {
@@ -867,10 +859,10 @@ export async function createMedipostActionLog({
hasError = false,
}: {
action:
| 'send_order_to_medipost'
| 'sync_analysis_results_from_medipost'
| 'send_fake_analysis_results_to_medipost'
| 'send_analysis_results_to_medipost';
| 'send_order_to_medipost'
| 'sync_analysis_results_from_medipost'
| 'send_fake_analysis_results_to_medipost'
| 'send_analysis_results_to_medipost';
xml: string;
hasAnalysisResults?: boolean;
medusaOrderId?: string | null;
@@ -891,3 +883,39 @@ export async function createMedipostActionLog({
.select('id')
.throwOnError();
}
async function getRelatedAnalysisElements({
analysisElements,
analyses,
currentGroup,
}: {
analysisElements: AnalysisElement[];
analyses: AnalysesWithGroupsAndElements;
currentGroup: {
created_at: string;
id: number;
name: string;
order: number;
original_id: string;
updated_at: string | null;
};
}) {
const relatedAnalysisElements: AnalysisElement[] = [];
const related1 = analysisElements?.filter(
(element) => element.analysis_groups.id === currentGroup.id,
);
if (related1) {
relatedAnalysisElements.push(...related1);
}
const related2 = analyses
?.filter(({ analysis_elements }) => analysis_elements.analysis_groups.id === currentGroup.id)
?.filter(({ analysis_elements }) => analysis_elements.analysis_groups.id === currentGroup.id)
?.flatMap(({ analysis_elements }) => analysis_elements);
if (related2) {
relatedAnalysisElements.push(...related2);
}
return relatedAnalysisElements;
}

View File

@@ -3,7 +3,6 @@
import {
getClientInstitution,
getClientPerson,
getOrderEnteredPerson,
getPais,
getPatient,
getProviderInstitution,
@@ -105,8 +104,7 @@ export async function composeOrderTestResponseXML({
<ValisTellimuseId>${orderId}</ValisTellimuseId>
${getClientInstitution({ index: 1 })}
${getProviderInstitution({ index: 1 })}
${getClientPerson()}
${getOrderEnteredPerson()}
${getClientPerson(person)}
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
${getPatient(person)}

View File

@@ -110,7 +110,7 @@ export async function handleNavigateToPayment({
const paymentLink =
await new MontonioOrderHandlerService().getMontonioPaymentLink({
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
returnUrl: `${env().siteUrl}/home/cart/montonio-callback`,
returnUrl: `${"https://webhook.site"}/home/cart/montonio-callback`,
amount: cart.total,
currency: cart.currency_code.toUpperCase(),
description: `Order from Medreport`,

View File

@@ -21,48 +21,70 @@ export const getPais = (
<Saaja>${recipient}</Saaja>
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
<SaadetisId>${orderId}</SaadetisId>
<Email>info@medreport.ee</Email>
<Email>argo@medreport.ee</Email>
</Pais>`;
};
export const getClientInstitution = ({ index }: { index?: number } = {}) => {
if (isProd) {
// return correct data
}
return `<Asutus tyyp="TELLIJA" ${index ? ` jarjenumber="${index}"` : ''}>
<AsutuseId>16381793</AsutuseId>
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
<AsutuseKood>MRP</AsutuseKood>
<AsutuseKood>TSU</AsutuseKood>
<Telefon>+37258871517</Telefon>
</Asutus>`;
};
export const getProviderInstitution = ({ index }: { index?: number } = {}) => {
if (isProd) {
// return correct data
}
return `<Asutus tyyp="TEOSTAJA" ${index ? ` jarjenumber="${index}"` : ''}>
<AsutuseId>11107913</AsutuseId>
<AsutuseNimi>Synlab Eesti OÜ</AsutuseNimi>
<AsutuseKood>HTI</AsutuseKood>
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
<AsutuseKood>SLA</AsutuseKood>
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
<Telefon>+37217123</Telefon>
<Telefon>+3723417123</Telefon>
</Asutus>`;
};
export const getClientPerson = () => {
export const getClientPerson = ({
idCode,
firstName,
lastName,
phone,
}: {
idCode: string,
firstName: string,
lastName: string,
phone: string,
}) => {
if (isProd) {
// return correct data
}
return `<Personal tyyp="TELLIJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
<Telefon>+37258131202</Telefon>
<PersonalKood>${idCode}</PersonalKood>
<PersonalPerekonnaNimi>${lastName}</PersonalPerekonnaNimi>
<PersonalEesNimi>${firstName}</PersonalEesNimi>
${phone ? `<Telefon>${phone.startsWith('+372') ? phone : `+372${phone}`}</Telefon>` : ''}
</Personal>`;
};
export const getOrderEnteredPerson = () => {
return `<Personal tyyp="SISESTAJA" jarjenumber="2">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
<Telefon>+37258131202</Telefon>
</Personal>`;
};
// export const getOrderEnteredPerson = () => {
// if (isProd) {
// // return correct data
// }
// return `<Personal tyyp="SISESTAJA" jarjenumber="1">
// <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
// <PersonalKood>D07907</PersonalKood>
// <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
// <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
// <Telefon>+37258131202</Telefon>
// </Personal>`;
// };
export const getPatient = ({
idCode,

View File

@@ -0,0 +1,5 @@
const list = ["prod_01K2JQF451ZKVV97FMX10T6DG1","prod_01K2JQFECMKR1CGDYQV81HB2W1","prod_01K2JQCZKZRZWD71CRN84V84NJ","prod_01K2JQD1AWQH7VHGPS4BA4028A","prod_01K2JQD321BMZTP7R4ZXEJNR17","prod_01K2JQD4RCRGERJRY8JQB7VWMT","prod_01K2JQD6F2VDANADSB5HY6WB6M","prod_01K2JQD85JGDRE0EJSQXGB74SE","prod_01K2JQD9VG391PZ02ZS57Y72PC","prod_01K2JQDBHYMESBB332PHF5TNTB", "prod_01K2JQG1EK4VTFH4GR2ZVB1RK6", "prod_01K2JQH0AMN407P1234MJ64BZM"]
const list2 = ['prod_01K2JQF451ZKVV97FMX10T6DG1', 'prod_01K2JQFECMKR1CGDYQV81HB2W1', 'prod_01K2JQCZKZRZWD71CRN84V84NJ', 'prod_01K2JQD1AWQH7VHGPS4BA4028A', 'prod_01K2JQD321BMZTP7R4ZXEJNR17', 'prod_01K2JQD4RCRGERJRY8JQB7VWMT', 'prod_01K2JQD6F2VDANADSB5HY6WB6M', 'prod_01K2JQD85JGDRE0EJSQXGB74SE', 'prod_01K2JQD9VG391PZ02ZS57Y72PC', 'prod_01K2JQDBHYMESBB332PHF5TNTB', 'prod_01K2JQG1EK4VTFH4GR2ZVB1RK6', 'prod_01K2JQH0AMN407P1234MJ64BZM']
console.log(list2.map(a => `'${a}'`).join(', '));

View File

@@ -0,0 +1,8 @@
function send_medipost_test_response() {
curl -X POST "$HOSTNAME/api/order/medipost-test-response" \
--header "x-jobs-api-key: $JOBS_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'" }'
}
#

View File

@@ -0,0 +1,16 @@
const SyncHelper = {
async send() {
await fetch('https://test.medreport.ee/api/order/medipost-test-response', {
method: "POST",
headers: { "x-jobs-api-key": "fd26ec26-70ed-11f0-9e95-431ac3b15a84", "content-type": "application/json" },
body: JSON.stringify({ "medusaOrderId": "order_01K2F3KC87NTMZX04T3KDZAQ69" }),
});
},
async sync() {
await fetch('https://test.medreport.ee/api/job/sync-analysis-results', {
method: "POST",
headers: { "x-jobs-api-key": "fd26ec26-70ed-11f0-9e95-431ac3b15a84" },
});
},
};
SyncHelper.sync()

View File

@@ -0,0 +1,157 @@
# Testing the Supabase Cron Job Setup
This guide provides step-by-step instructions to test your Supabase cron job configuration.
## Quick Setup Commands
### 1. Deploy the Migration (Option A)
If you want to use the migration approach:
```bash
# Make sure you're connected to your Supabase project
npm run supabase:deploy
```
Then manually update the migration file with your actual values before deploying.
### 2. Manual Setup (Option B - Recommended)
Use the SQL Editor in Supabase Dashboard:
1. Go to your Supabase Dashboard → Database → SQL Editor
2. Copy and paste the content from `supabase/sql/setup-cron-job.sql`
3. Run the SQL to create the function
4. Then execute the schedule function with your actual values:
```sql
select schedule_sync_analysis_results_cron(
'https://your-production-domain.com', -- Your actual API URL
'your-actual-jobs-api-token' -- Your actual JOBS_API_TOKEN
);
```
## Testing Steps
### 1. Verify Extensions are Enabled
```sql
select * from pg_extension where extname in ('pg_cron', 'pg_net');
```
Expected result: Both `pg_cron` and `pg_net` should be listed.
### 2. Check Job is Scheduled
```sql
select * from cron.job where jobname = 'sync-analysis-results-every-15-minutes';
```
Expected result: One row with your job details, `active` should be `true`.
### 3. Test API Endpoint Manually
Before relying on the cron job, test your API endpoint manually:
```bash
curl -X POST https://your-domain.com/api/job/sync-analysis-results \
-H "Content-Type: application/json" \
-H "x-jobs-api-key: YOUR_JOBS_API_TOKEN" \
-v
```
Expected result: Status 200 with success message.
### 4. Monitor Job Execution
Wait for the job to run (up to 15 minutes), then check execution history:
```sql
select
job_run_details.*,
job.jobname
from cron.job_run_details
join cron.job on job.jobid = job_run_details.jobid
where job.jobname = 'sync-analysis-results-every-15-minutes'
order by start_time desc
limit 5;
```
### 5. Check Application Logs
Monitor your application logs to see if the API calls are being received and processed successfully.
## Environment Variables Required
Make sure these environment variables are set in your production environment:
- `JOBS_API_TOKEN` - The API key for authenticating job requests
- All other environment variables required by your `sync-analysis-results` handler
## Common Issues and Solutions
### Issue 1: Job Not Appearing
**Problem**: Job doesn't appear in `cron.job` table.
**Solution**:
- Check if you have sufficient permissions
- Ensure extensions are enabled
- Try running the schedule function again
### Issue 2: Job Scheduled but Not Running
**Problem**: Job appears in table but no execution history.
**Solutions**:
- Check if `active` is `true` in `cron.job` table
- Verify cron schedule format is correct
- Check Supabase logs for any cron-related errors
### Issue 3: HTTP Requests Failing
**Problem**: Job runs but API calls fail.
**Solutions**:
- Test API endpoint manually with curl
- Verify API URL is correct and accessible from Supabase
- Check if `JOBS_API_TOKEN` is correct
- Ensure your application is deployed and running
### Issue 4: Authentication Errors
**Problem**: Getting 401 Unauthorized responses.
**Solutions**:
- Verify `x-jobs-api-key` header is included
- Check that `JOBS_API_TOKEN` matches between cron job and application
- Ensure the header name is exactly `x-jobs-api-key` (case-sensitive)
## Cleanup Commands
If you need to remove the cron job:
```sql
-- Unschedule the job
select cron.unschedule('sync-analysis-results-every-15-minutes');
-- Drop the helper function (optional)
drop function if exists schedule_sync_analysis_results_cron(text, text);
```
## Next Steps
Once the cron job is working:
1. Remove any old instrumentation.ts cron logic if it exists
2. Monitor the job performance and adjust interval if needed
3. Set up alerting for failed job executions
4. Consider adding more detailed logging to your API endpoint
## Support
If you encounter issues:
1. Check the troubleshooting section in `docs/supabase-cron-setup.md`
2. Review Supabase documentation for pg_cron and pg_net
3. Contact your team for deployment-specific configuration details

File diff suppressed because it is too large Load Diff

138
local-sync/xmls/curl2.sh Executable file
View File

@@ -0,0 +1,138 @@
curl --location 'https://meditest.medisoft.ee:7443/Medipost/MedipostServlet' \
--form 'Action="SendPrivateMessage";type=text/plain; charset=UTF-8' \
--form 'User="trvurgtst";type=text/plain; charset=UTF-8' \
--form 'Password="SRB48HZMV";type=text/plain; charset=UTF-8' \
--form 'Receiver="trvurgtst";type=text/plain; charset=UTF-8' \
--form 'MessageType="Tellimus";type=text/plain; charset=UTF-8' \
--form 'Message="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Saadetis xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"TellimusLOINC.xsd\">
<Pais>
<Pakett versioon=\"20\">OL</Pakett>
<Saatja>trvurgtst</Saatja>
<Saaja>trvurgtst</Saaja>
<Aeg>2022-07-22 11:31:57</Aeg>
<SaadetisId>234254234</SaadetisId>
<Email>info@terviseuuringud.ee</Email>
</Pais>
<Tellimus cito=\"EI\">
<ValisTellimuseId>1288</ValisTellimuseId>
<\!--<TellijaAsutus>-->
<Asutus tyyp=\"TELLIJA\">
<AsutuseId>12702440</AsutuseId>
<AsutuseNimi>Health Tests OÜ</AsutuseNimi>
<AsutuseKood>TSU</AsutuseKood>
<AllyksuseNimi/>
<Telefon>+37256257117</Telefon>
<Vald>0387</Vald>
<Aadress>Valukoja 10, 11415 Tallinn</Aadress>
</Asutus>
<\!--<TeostajaAsutus>-->
<Asutus tyyp=\"TEOSTAJA\">
<AsutuseId>12702440</AsutuseId>
<AsutuseNimi>Health Tests OÜ</AsutuseNimi>
<AsutuseKood>TSU</AsutuseKood>
<AllyksuseNimi/>
<Telefon>+37256257117</Telefon>
<Vald>0387</Vald>
<Aadress>Valukoja 10, 11415 Tallinn</Aadress>
</Asutus>
<\!--<TellijaIsik>-->
<Personal tyyp=\"TELLIJA\">
<\!--Tervishoiutöötaja kood (OID: 1.3.6.1.4.1.28284.6.2.4.9)
või Eesti isikukood (OID: 1.3.6.1.4.1.28284.6.2.2.1) -->
<PersonalOID>1.3.6.1.4.1.28284.6.2.2.1</PersonalOID>
<PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Tsvetkov</PersonalPerekonnaNimi>
<PersonalEesNimi>Eduard</PersonalEesNimi>
<Telefon>+3725555000</Telefon>
</Personal>
<\!--<SisestajaIsik>-->
<Personal tyyp=\"SISESTAJA\">
<\!--Tervishoiutöötaja kood (OID: 1.3.6.1.4.1.28284.6.2.4.9)
või Eesti isikukood (OID: 1.3.6.1.4.1.28284.6.2.2.1) -->
<PersonalOID>1.3.6.1.4.1.28284.6.2.2.1</PersonalOID>
<PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Tsvetkov</PersonalPerekonnaNimi>
<PersonalEesNimi>Eduard</PersonalEesNimi>
</Personal>
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
<Patsient>
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
<Isikukood>37907262736</Isikukood>
<PerekonnaNimi>KIVIRÜÜT</PerekonnaNimi>
<EesNimi>ARGO</EesNimi>
<SynniAeg>1979-07-26</SynniAeg>
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
<Sugu>M</Sugu>
</Patsient>
<Konfidentsiaalsus>
<PatsiendileOID>2.16.840.1.113883.5.25</PatsiendileOID>
<Patsiendile>N</Patsiendile>
<ArstileOID>1.3.6.1.4.1.28284.6.2.2.39.1</ArstileOID>
<Arstile>N</Arstile>
<EsindajaleOID>1.3.6.1.4.1.28284.6.2.2.37.1</EsindajaleOID>
<Esindajale>N</Esindajale>
</Konfidentsiaalsus>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.4.100</ProovinouIdOID>
<ProovinouId>ANI7570-16522287</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.8</MaterjaliTyypOID>
<MaterjaliTyyp>119297000</MaterjaliTyyp>
<MaterjaliNimi>Veri</MaterjaliNimi>
<Ribakood>16522287</Ribakood>
<Jarjenumber>7570</Jarjenumber>
<VotmisAeg>2022-06-13 08:53:00</VotmisAeg>
</Proov>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.4.100</ProovinouIdOID>
<ProovinouId>ANI7571-16522288</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.8</MaterjaliTyypOID>
<MaterjaliTyyp>119297000</MaterjaliTyyp>
<MaterjaliNimi>Veri</MaterjaliNimi>
<Ribakood>16522288</Ribakood>
<Jarjenumber>7571</Jarjenumber>
<VotmisAeg>2022-06-13 08:53:00</VotmisAeg>
</Proov>
<UuringuGrupp>
<UuringuGruppId>TL10</UuringuGruppId>
<UuringuGruppNimi>Hematoloogilised uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>57021-8</UuringId>
<TLyhend>B-CBC-5Diff</TLyhend>
<KNimetus>Hemogramm 5-osalise leukogrammiga</KNimetus>
<UuringNimi>Hemogramm</UuringNimi>
<TellijaUuringId>18327</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>7570</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
<UuringuGrupp>
<UuringuGruppId>TL40</UuringuGruppId>
<UuringuGruppNimi>Kliinilise keemia uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>L-3757</UuringId>
<TLyhend>B-HbA1c panel</TLyhend>
<KNimetus>HbA1c paneel</KNimetus>
<UuringNimi>HbA1c</UuringNimi>
<TellijaUuringId>18349</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>7571</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
</Tellimus>
</Saadetis>
";type=text/xml; charset=UTF-8'

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
<Pais>
<Pakett versioon="20">OL</Pakett>
<Saatja>trvurgtst</Saatja>
<Saaja>trvurgtst</Saaja>
<Aeg>2025-08-04 03:30:15</Aeg>
<SaadetisId>
1</SaadetisId>
<Email>argo@medreport.ee</Email>
</Pais>
<Tellimus cito="EI">
<ValisTellimuseId>
1</ValisTellimuseId>
<!--<TellijaAsutus>-->
<Asutus tyyp="TELLIJA">
<AsutuseId>16381793</AsutuseId>
<AsutuseNimi>MedReport
</AsutuseNimi>
<AsutuseKood>TSU</AsutuseKood>
<Telefon>+37258871517</Telefon>
</Asutus>
<!--<TeostajaAsutus>-->
<Asutus tyyp="TEOSTAJA">
<AsutuseId>11107913</AsutuseId>
<AsutuseNimi>Synlab HTI
Tallinn</AsutuseNimi>
<AsutuseKood>SLA</AsutuseKood>
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
<Telefon>+3723417123</Telefon>
</Asutus>
<!--<TellijaIsik>-->
<Personal tyyp="TELLIJA"
jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>
39610230904</PersonalKood>
<PersonalPerekonnaNimi>test2</PersonalPerekonnaNimi>
<PersonalEesNimi>
test1</PersonalEesNimi>
<Telefon>56232775</Telefon>
</Personal>
<TellijaMarkused>Test
comment</TellijaMarkused>
<Patsient>
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
<Isikukood>39610230904</Isikukood>
<PerekonnaNimi>test2</PerekonnaNimi>
<EesNimi>
test1</EesNimi>
<SynniAeg>1996-00-23</SynniAeg>
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
<Sugu>male</Sugu>
</Patsient>
<Konfidentsiaalsus>
<PatsiendileOID>
2.16.840.1.113883.5.25</PatsiendileOID>
<Patsiendile>N</Patsiendile>
<ArstileOID>
1.3.6.1.4.1.28284.6.2.2.39.1</ArstileOID>
<Arstile>N</Arstile>
<EsindajaleOID>
1.3.6.1.4.1.28284.6.2.2.37.1</EsindajaleOID>
<Esindajale>N</Esindajale>
</Konfidentsiaalsus>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.1.243.16</ProovinouIdOID>
<ProovinouId>A2</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.10</MaterjaliTyypOID>
<MaterjaliTyyp>119364003</MaterjaliTyyp>
<MaterjaliNimi>Seerum</MaterjaliNimi>
<Jarjenumber>1</Jarjenumber>
</Proov>
<UuringuGrupp>
<UuringuGruppId>TL106</UuringuGruppId>
<UuringuGruppNimi>Söömishäirete uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>2276-4</UuringId>
<TLyhend>S,P-Fer</TLyhend>
<KNimetus>Ferritiin</KNimetus>
<UuringNimi>Ferritiin</UuringNimi>
<TellijaUuringId>84</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>1</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
</Tellimus>
</Saadetis>

View File

@@ -0,0 +1,76 @@
<?xml version= \"1.0\" encoding= \"UTF-8\"?>
<Saadetis xmlns:xsi= \"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation= \"TellimusLOINC.xsd\">
<Pais>
<Pakett versioon= \"20\">OL</Pakett>
<Saatja>trvurgtst</Saatja>
<Saaja>trvurgtst</Saaja>
<Aeg>2025-08-04 06:22:18</Aeg>
<SaadetisId>TSU000001200</SaadetisId>
<Email>argo@medreport.ee</Email>
</Pais>
<Vastus>
<ValisTellimuseId>TSU000001200</ValisTellimuseId>
<Asutus tyyp= \"TELLIJA\" jarjenumber= \"1\">
<AsutuseId>16381793</AsutuseId>
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
<AsutuseKood>TSU</AsutuseKood>
<Telefon>+37258871517</Telefon>
</Asutus>
<Asutus tyyp= \"TEOSTAJA\" jarjenumber= \"1\">
<AsutuseId>11107913</AsutuseId>
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
<AsutuseKood>SLA</AsutuseKood>
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
<Telefon>+3723417123</Telefon>
</Asutus>
<Personal tyyp= \"TELLIJA\" jarjenumber= \"1\">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>39610230903</PersonalKood>
<PersonalPerekonnaNimi>User</PersonalPerekonnaNimi>
<PersonalEesNimi>Test</PersonalEesNimi>
<Telefon>+37256232775</Telefon>
</Personal>
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
<TellimuseNumber>TSU000001200</TellimuseNumber>
<TellimuseOlek>4</TellimuseOlek>
<Patsient>
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
<Isikukood>39610230903</Isikukood>
<PerekonnaNimi>User</PerekonnaNimi>
<EesNimi>Test</EesNimi>
<SynniAeg>1996-00-23</SynniAeg>
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
<Sugu>male</Sugu>
</Patsient>
<UuringuGrupp>
<UuringuGruppId>TL106</UuringuGruppId>
<UuringuGruppNimi>Söömishäirete uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>2276-4</UuringId>
<TLyhend>S,P-Fer</TLyhend>
<KNimetus>Ferritiin</KNimetus>
<UuringNimi>Ferritiin</UuringNimi>
<TellijaUuringId>84</TellijaUuringId>
<TeostajaUuringId>84</TeostajaUuringId>
<UuringOlek>4</UuringOlek>
<Mootyhik>%</Mootyhik>
<UuringuVastus>
<VastuseVaartus>30000</VastuseVaartus>
<VastuseAeg>2025-08-04 07:00:12</VastuseAeg>
<NormYlem kaasaarvatud= \"EI\">100000</NormYlem>
<NormAlum kaasaarvatud= \"EI\">50</NormAlum>
<NormiStaatus>0</NormiStaatus>
<ProoviJarjenumber>1</ProoviJarjenumber>
</UuringuVastus>
</UuringuElement>
<UuringuTaitjaAsutuseJnr>2</UuringuTaitjaAsutuseJnr>
</Uuring>
</UuringuGrupp>
</Vastus>
</Saadetis>

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ const config = {
},
experimental: {
mdxRs: true,
reactCompiler: false,
reactCompiler: ENABLE_REACT_COMPILER,
optimizePackageImports: [
'recharts',
'lucide-react',

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
<Pais>
<Pakett versioon="20">OL</Pakett>
<Saatja>medreport</Saatja>
<Saaja>HTI</Saaja>
<Aeg>2025-09-04 14:04:33</Aeg>
<SaadetisId>1234567890</SaadetisId>
<Email>argo@medreport.ee</Email>
</Pais>
<Tellimus cito="EI">
<ValisTellimuseId>1234567890</ValisTellimuseId>
<!--<TellijaAsutus>-->
<Asutus tyyp="TELLIJA">
<AsutuseId>16381793</AsutuseId>
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
<AsutuseKood>TSU</AsutuseKood>
<Telefon>+37258871517</Telefon>
</Asutus>
<!--<TeostajaAsutus>-->
<Asutus tyyp="TEOSTAJA">
<AsutuseId>11107913</AsutuseId>
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
<AsutuseKood>SLA</AsutuseKood>
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
<Telefon>+3723417123</Telefon>
</Asutus>
<!--<TellijaIsik>-->
<Personal tyyp="TELLIJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>1234567890</PersonalKood>
<PersonalPerekonnaNimi>Doe</PersonalPerekonnaNimi>
<PersonalEesNimi>John</PersonalEesNimi>
<Telefon>+3721234567890</Telefon>
</Personal>
<TellijaMarkused></TellijaMarkused>
<Patsient>
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
<Isikukood>1234567890</Isikukood>
<PerekonnaNimi>Doe</PerekonnaNimi>
<EesNimi>John</EesNimi>
<SynniAeg>1826-00-06</SynniAeg>
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
<Sugu>M</Sugu>
</Patsient>
<Konfidentsiaalsus>
<PatsiendileOID>2.16.840.1.113883.5.25</PatsiendileOID>
<Patsiendile>N</Patsiendile>
<ArstileOID>1.3.6.1.4.1.28284.6.2.2.39.1</ArstileOID>
<Arstile>N</Arstile>
<EsindajaleOID>1.3.6.1.4.1.28284.6.2.2.37.1</EsindajaleOID>
<Esindajale>N</Esindajale>
</Konfidentsiaalsus>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.1.243.22</ProovinouIdOID>
<ProovinouId>A7</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.15</MaterjaliTyypOID>
<MaterjaliTyyp>445295009</MaterjaliTyyp>
<MaterjaliNimi>K2E/K3E-veri</MaterjaliNimi>
<Jarjenumber>1</Jarjenumber>
</Proov>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.1.243.22</ProovinouIdOID>
<ProovinouId>A2</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.15</MaterjaliTyypOID>
<MaterjaliTyyp>119364003</MaterjaliTyyp>
<MaterjaliNimi>Seerum</MaterjaliNimi>
<Jarjenumber>2</Jarjenumber>
</Proov>
<UuringuGrupp>
<UuringuGruppId>TL10</UuringuGruppId>
<UuringuGruppNimi>Hematoloogilised uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>57021-8</UuringId>
<TLyhend>B-CBC-5Diff</TLyhend>
<KNimetus>Hemogramm 5-osalise leukogrammiga</KNimetus>
<UuringNimi>Hemogramm</UuringNimi>
<TellijaUuringId>4522</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>1</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
<UuringuGrupp>
<UuringuGruppId>TL50</UuringuGruppId>
<UuringuGruppNimi>Hormoon- jm. immuunuuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>2276-4</UuringId>
<TLyhend>S,P-Fer</TLyhend>
<KNimetus>Ferritiin</KNimetus>
<UuringNimi>Ferritiin</UuringNimi>
<TellijaUuringId>4605</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>2</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
</Tellimus>
</Saadetis>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
<Pais>
<Pakett versioon="20">OL</Pakett>
<Saatja>medreport</Saatja>
<Saaja>HTI</Saaja>
<Aeg>2025-09-04 14:17:12</Aeg>
<SaadetisId>1234567890</SaadetisId>
<Email>argo@medreport.ee</Email>
</Pais>
<Tellimus cito="EI">
<ValisTellimuseId>1234567890</ValisTellimuseId>
<!--<TellijaAsutus>-->
<Asutus tyyp="TELLIJA">
<AsutuseId>16381793</AsutuseId>
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
<AsutuseKood>TSU</AsutuseKood>
<Telefon>+37258871517</Telefon>
</Asutus>
<!--<TeostajaAsutus>-->
<Asutus tyyp="TEOSTAJA">
<AsutuseId>11107913</AsutuseId>
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
<AsutuseKood>SLA</AsutuseKood>
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
<Telefon>+3723417123</Telefon>
</Asutus>
<!--<TellijaIsik>-->
<Personal tyyp="TELLIJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>1234567890</PersonalKood>
<PersonalPerekonnaNimi>Doe</PersonalPerekonnaNimi>
<PersonalEesNimi>John</PersonalEesNimi>
<Telefon>+3721234567890</Telefon>
</Personal>
<TellijaMarkused></TellijaMarkused>
<Patsient>
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
<Isikukood>1234567890</Isikukood>
<PerekonnaNimi>Doe</PerekonnaNimi>
<EesNimi>John</EesNimi>
<SynniAeg>1826-00-06</SynniAeg>
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
<Sugu>M</Sugu>
</Patsient>
<Konfidentsiaalsus>
<PatsiendileOID>2.16.840.1.113883.5.25</PatsiendileOID>
<Patsiendile>N</Patsiendile>
<ArstileOID>1.3.6.1.4.1.28284.6.2.2.39.1</ArstileOID>
<Arstile>N</Arstile>
<EsindajaleOID>1.3.6.1.4.1.28284.6.2.2.37.1</EsindajaleOID>
<Esindajale>N</Esindajale>
</Konfidentsiaalsus>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.1.243.22</ProovinouIdOID>
<ProovinouId>A9</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.15</MaterjaliTyypOID>
<MaterjaliTyyp>2491000181101</MaterjaliTyyp>
<MaterjaliNimi>Glükolüüsi inhibiitoriga plasma</MaterjaliNimi>
<Jarjenumber>1</Jarjenumber>
</Proov>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.1.243.22</ProovinouIdOID>
<ProovinouId>A2</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.15</MaterjaliTyypOID>
<MaterjaliTyyp>119364003</MaterjaliTyyp>
<MaterjaliNimi>Seerum</MaterjaliNimi>
<Jarjenumber>2</Jarjenumber>
</Proov>
<Proov>
<ProovinouIdOID>1.3.6.1.4.1.28284.6.2.1.243.22</ProovinouIdOID>
<ProovinouId>A2</ProovinouId>
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.15</MaterjaliTyypOID>
<MaterjaliTyyp>119364003</MaterjaliTyyp>
<MaterjaliNimi>Seerum</MaterjaliNimi>
<Jarjenumber>3</Jarjenumber>
</Proov>
<UuringuGrupp>
<UuringuGruppId>TL40</UuringuGruppId>
<UuringuGruppNimi>Kliinilise keemia uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>14771-0</UuringId>
<TLyhend>fS,fP-Gluc</TLyhend>
<KNimetus>Glükoos paastuseerumis/-plasmas</KNimetus>
<UuringNimi>Glükoos</UuringNimi>
<TellijaUuringId>4530</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>1</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
<UuringuGrupp>
<UuringuGruppId>TL40</UuringuGruppId>
<UuringuGruppNimi>Kliinilise keemia uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>14927-8</UuringId>
<TLyhend>S,P-Trigl</TLyhend>
<KNimetus>Triglütseriidid</KNimetus>
<UuringNimi>Triglütseriidid</UuringNimi>
<TellijaUuringId>4535</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>2</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
<UuringuGrupp>
<UuringuGruppId>TL40</UuringuGruppId>
<UuringuGruppNimi>Kliinilise keemia uuringud</UuringuGruppNimi>
<Uuring>
<UuringuElement>
<UuringIdOID>2.16.840.1.113883.6.1</UuringIdOID>
<UuringId>14798-3</UuringId>
<TLyhend>S,P-Fe</TLyhend>
<KNimetus>Raud</KNimetus>
<UuringNimi>Raud</UuringNimi>
<TellijaUuringId>4570</TellijaUuringId>
</UuringuElement>
<ProoviJarjenumber>3</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>
</Tellimus>
</Saadetis>

View File

@@ -28,6 +28,10 @@ export const OrderSchema = z.object({
title: z.string(),
isPackage: z.boolean(),
analysisOrderId: z.number(),
productMetadata: z.object({
analysisIdOriginal: z.string().nullable(),
analysisResultUnit: z.string().nullable(),
}).nullable(),
});
export type Order = z.infer<typeof OrderSchema>;

View File

@@ -87,10 +87,7 @@ export async function getOrSetCart(countryCode: string) {
return cart;
}
export async function updateCart(
{ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string },
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
) {
export async function updateCart({ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string }) {
const cartId = id || (await getCartId());
if (!cartId) {
@@ -112,13 +109,9 @@ export async function updateCart(
const fulfillmentCacheTag = await getCacheTag("fulfillment");
revalidateTag(fulfillmentCacheTag);
onSuccess();
return cart;
})
.catch((e) => {
onError();
return medusaError(e);
});
.catch(medusaError);
}
export async function addToCart({
@@ -266,10 +259,7 @@ export async function initiatePaymentSession(
.catch(medusaError);
}
export async function applyPromotions(
codes: string[],
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
) {
export async function applyPromotions(codes: string[]) {
const cartId = await getCartId();
if (!cartId) {
@@ -288,13 +278,8 @@ export async function applyPromotions(
const fulfillmentCacheTag = await getCacheTag("fulfillment");
revalidateTag(fulfillmentCacheTag);
onSuccess();
})
.catch((e) => {
onError();
return medusaError(e);
});
.catch(medusaError);
}
export async function applyGiftCard(code: string) {
@@ -442,7 +427,7 @@ export async function placeOrder(cartId?: string, options: { revalidateCacheTags
} else {
throw new Error("Cart is not an order");
}
return retrieveOrder(cartRes.order.id);
}

View File

@@ -68,7 +68,7 @@ export const listProducts = async ({
},
headers,
next,
cache: "force-cache",
//cache: "force-cache",
}
)
.then(({ products, count }) => {

View File

@@ -5,8 +5,8 @@ import type { User } from '@supabase/supabase-js';
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
import { ApplicationRole } from '@kit/accounts/types/accounts';
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import { useUser } from '@kit/supabase/hooks/use-user';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
const paths = {
home: pathsConfig.app.home,

View File

@@ -68,7 +68,7 @@
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"orContinueWith": "Or continue with",
"orContinueWith": "Continue with",
"redirecting": "You're in! Please wait...",
"errors": {
"Invalid login credentials": "The credentials entered are invalid",

View File

@@ -5,7 +5,6 @@
"emptyCartMessageDescription": "Add items to your cart to continue.",
"subtotal": "Subtotal",
"total": "Total",
"promotionsTotal": "Promotions total",
"table": {
"item": "Item",
"quantity": "Quantity",
@@ -25,13 +24,10 @@
"timeoutAction": "Continue"
},
"discountCode": {
"title": "Gift card or promotion code",
"label": "Add Promotion Code(s)",
"apply": "Apply",
"subtitle": "If you wish, you can add a promotion code",
"placeholder": "Enter promotion code",
"remove": "Remove promotion code",
"appliedCodes": "Promotion(s) applied:"
"placeholder": "Enter promotion code"
},
"items": {
"synlabAnalyses": {

View File

@@ -68,7 +68,7 @@
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
"termsOfService": "Kasutustingimused",
"privacyPolicy": "Privaatsuspoliitika",
"orContinueWith": "Või jätka koos",
"orContinueWith": "Jätka",
"redirecting": "Oled sees! Palun oota...",
"errors": {
"Invalid login credentials": "Sisestatud andmed on valed",

View File

@@ -4,7 +4,6 @@
"emptyCartMessage": "Sinu ostukorv on tühi",
"emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.",
"subtotal": "Vahesumma",
"promotionsTotal": "Soodustuse summa",
"total": "Summa",
"table": {
"item": "Toode",
@@ -29,13 +28,7 @@
"label": "Lisa promo kood",
"apply": "Rakenda",
"subtitle": "Kui soovid, võid lisada promo koodi",
"placeholder": "Sisesta promo kood",
"remove": "Eemalda promo kood",
"appliedCodes": "Rakendatud sooduskoodid:",
"removeError": "Sooduskoodi eemaldamine ebaõnnestus",
"removeSuccess": "Sooduskood eemaldatud",
"addError": "Sooduskoodi rakendamine ebaõnnestus",
"addSuccess": "Sooduskood rakendatud"
"placeholder": "Sisesta promo kood"
},
"items": {
"synlabAnalyses": {

View File

@@ -68,7 +68,7 @@
"acceptTermsAndConditions": "Я принимаю <TermsOfServiceLink /> и <PrivacyPolicyLink />",
"termsOfService": "Условия использования",
"privacyPolicy": "Политика конфиденциальности",
"orContinueWith": "Или продолжить через",
"orContinueWith": "Продолжить через",
"redirecting": "Вы вошли! Пожалуйста, подождите...",
"errors": {
"Invalid login credentials": "Введенные данные недействительны",

8
run-test-sync-local.sh Normal file → Executable file
View File

@@ -1,6 +1,6 @@
#!/bin/bash
MEDUSA_ORDER_ID="order_01K1TQQHZGPXKDHAH81TDSNGXR"
MEDUSA_ORDER_ID="order_01K2JFCZ609YF5G84ZKDCBWPM6"
# HOSTNAME="https://test.medreport.ee"
# JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84"
@@ -33,7 +33,7 @@ function sync_analysis_groups_store() {
# Requirements
# 1. Sync analysis groups from Medipost to B2B
sync_analysis_groups
#sync_analysis_groups
# 2. Optional - sync all Medipost analysis groups from B2B to Medusa (or add manually)
#sync_analysis_groups_store
@@ -41,7 +41,7 @@ sync_analysis_groups
# 3. Set up products configurations in Medusa so B2B "Telli analüüs" page shows the product and you can do payment flow
# 4. After payment is done, run `send_medipost_test_response` to send the fake test results to Medipost
# send_medipost_test_response
#send_medipost_test_response
# 5. Run `sync_analysis_results` to sync the all new Medipost results to B2B
# sync_analysis_results
sync_analysis_results

5
scripts/build-docker.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
docker build -t medreport-b2b:latest .
# Get size of built image and display it

View File

@@ -105,9 +105,9 @@ file_size_limit = "50MiB"
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
site_url = "http://localhost:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000","http://localhost:3000/auth/callback", "http://localhost:3000/update-password"]
additional_redirect_urls = ["https://127.0.0.1:3000","http://127.0.0.1:3000","http://localhost:3000","http://localhost:3000/auth/callback", "http://localhost:3000/update-password"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
@@ -269,6 +269,14 @@ url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
[auth.external.keycloak]
enabled = true
client_id = "env(SUPABASE_AUTH_CLIENT_ID)"
secret = "env(SUPABASE_AUTH_KEYCLOAK_SECRET)"
redirect_uri = "env(SUPABASE_AUTH_KEYCLOAK_CALLBACK_URL)"
url = "env(SUPABASE_AUTH_KEYCLOAK_URL)"
skip_nonce_check = true
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false

View File

@@ -0,0 +1,64 @@
-- Enable required extensions for cron jobs and HTTP requests
create extension if not exists pg_cron;
create extension if not exists pg_net;
-- Enable the cron extension to be used in all databases
-- This needs to be run with superuser privileges or via Supabase Dashboard
grant usage on schema cron to postgres;
grant all privileges on all tables in schema cron to postgres;
-- Function to safely schedule the cron job with environment variables
-- This approach allows for easier management and avoids hardcoding sensitive values
create or replace function schedule_sync_analysis_results_cron(
api_url text,
api_token text
) returns text as $$
declare
job_exists boolean;
begin
-- Check if job already exists
select exists(
select 1 from cron.job
where jobname = 'sync-analysis-results-every-15-minutes'
) into job_exists;
-- If job exists, unschedule it first
if job_exists then
perform cron.unschedule('sync-analysis-results-every-15-minutes');
end if;
-- Schedule the new job
perform cron.schedule(
'sync-analysis-results-every-15-minutes',
'*/15 * * * *', -- Every 15 minutes
format(
'select net.http_post(url := ''%s/api/job/sync-analysis-results'', headers := jsonb_build_object(''Content-Type'', ''application/json'', ''x-jobs-api-key'', ''%s''), body := jsonb_build_object(''triggered_by'', ''supabase_cron'', ''timestamp'', now())) as request_id;',
api_url, api_token
)
);
return 'Cron job scheduled successfully';
end;
$$ language plpgsql;
-- Example usage (replace with your actual values):
-- select schedule_sync_analysis_results_cron(
-- 'https://your-domain.com',
-- 'your-jobs-api-token-here'
-- );
-- Utility queries for managing the cron job:
-- 1. Check if the job is scheduled
-- select * from cron.job where jobname = 'sync-analysis-results-every-15-minutes';
-- 2. View job execution history
-- select * from cron.job_run_details
-- where jobid = (select jobid from cron.job where jobname = 'sync-analysis-results-every-15-minutes')
-- order by start_time desc limit 10;
-- 3. Unschedule the job if needed
-- select cron.unschedule('sync-analysis-results-every-15-minutes');
-- 4. Check all scheduled jobs
-- select jobid, schedule, active, jobname from cron.job;

View File

@@ -0,0 +1,122 @@
# Supabase Cron Job Setup for sync-analysis-results
This document explains how to set up a Supabase cron job to automatically call the `sync-analysis-results` API endpoint every 15 minutes.
## Prerequisites
1. Supabase project with database access
2. Your application deployed and accessible via a public URL
3. `JOBS_API_TOKEN` environment variable configured
## Setup Steps
### 1. Enable Required Extensions
First, enable the required PostgreSQL extensions in your Supabase project:
```sql
create extension if not exists pg_cron;
create extension if not exists pg_net;
```
You can run this either:
- In the Supabase Dashboard → Database → SQL Editor
- Or deploy the migration file: `supabase/migrations/setup_sync_analysis_results_cron.sql`
### 2. Schedule the Cron Job
Use the helper function to schedule the cron job with your specific configuration:
```sql
select schedule_sync_analysis_results_cron(
'https://your-actual-domain.com', -- Replace with your API URL
'your-actual-jobs-api-token' -- Replace with your JOBS_API_TOKEN
);
```
### 3. Verify the Setup
Check if the job was scheduled successfully:
```sql
select * from cron.job where jobname = 'sync-analysis-results-every-15-minutes';
```
## Management Commands
### View Job Execution History
```sql
select * from cron.job_run_details
where jobid = (select jobid from cron.job where jobname = 'sync-analysis-results-every-15-minutes')
order by start_time desc limit 10;
```
### Check All Scheduled Jobs
```sql
select jobid, schedule, active, jobname from cron.job;
```
### Unschedule the Job
```sql
select cron.unschedule('sync-analysis-results-every-15-minutes');
```
## Configuration Details
- **Schedule**: `*/15 * * * *` (every 15 minutes)
- **HTTP Method**: POST
- **Headers**:
- `Content-Type: application/json`
- `x-jobs-api-key: YOUR_JOBS_API_TOKEN`
- **Body**: JSON with metadata about the cron trigger
## Security Considerations
1. **API Token**: Store your `JOBS_API_TOKEN` securely and never commit it to version control
2. **Network Access**: Ensure your Supabase instance can reach your deployed application
3. **Rate Limiting**: The 15-minute interval should be appropriate for your use case
## Troubleshooting
### Job Not Running
1. Check if extensions are enabled:
```sql
select * from pg_extension where extname in ('pg_cron', 'pg_net');
```
2. Verify job is active:
```sql
select * from cron.job where jobname = 'sync-analysis-results-every-15-minutes';
```
3. Check for execution errors:
```sql
select * from cron.job_run_details
where jobid = (select jobid from cron.job where jobname = 'sync-analysis-results-every-15-minutes')
and status = 'failed'
order by start_time desc;
```
### API Authentication Issues
1. Verify your `JOBS_API_TOKEN` is correct
2. Test the API endpoint manually:
```bash
curl -X POST https://your-domain.com/api/job/sync-analysis-results \
-H "Content-Type: application/json" \
-H "x-jobs-api-key: YOUR_JOBS_API_TOKEN"
```
## Alternative: Using Supabase Edge Functions
If you prefer using Supabase Edge Functions instead of pg_cron, you can:
1. Create an Edge Function that calls your API
2. Use Supabase's built-in cron triggers for Edge Functions
3. This approach provides better logging and error handling
Contact your team lead for assistance with Edge Functions setup if needed.