Compare commits
36 Commits
keycloak2
...
a89d8d3153
| Author | SHA1 | Date | |
|---|---|---|---|
| a89d8d3153 | |||
| 1b29cb222b | |||
| dfcfdb8f97 | |||
| d87d08aaea | |||
| c83694222d | |||
| c08fe26b36 | |||
| d3202a2cb2 | |||
| 2435e6f113 | |||
| 54856b0e45 | |||
| 95e72bb3f8 | |||
| 3d268b6061 | |||
| 5c6280ec42 | |||
| 0de9dcf7e3 | |||
| f3a6fb627c | |||
| c1746c6c20 | |||
| c6f56f6e11 | |||
| a6b246cdf3 | |||
| a705dea9cf | |||
| 8485d2e9a3 | |||
| c356f69656 | |||
| cacd23be40 | |||
| f8765dce49 | |||
| 42bebb6d93 | |||
| 354a0c04ee | |||
| 72bb9a33ef | |||
| 771c28f8ef | |||
|
|
e9497c3d52 | ||
| 70188f297f | |||
| e0940a1600 | |||
| 65eb6c780d | |||
| 6e9cde6b95 | |||
| 3a062eaa9c | |||
| c07acb85a2 | |||
| 1de564b917 | |||
| a2c080914a | |||
|
|
94dd00b9ca |
@@ -1,2 +0,0 @@
|
|||||||
.git
|
|
||||||
Dockerfile
|
|
||||||
5
.env
5
.env
@@ -13,7 +13,7 @@ NEXT_PUBLIC_THEME_COLOR="#ffffff"
|
|||||||
NEXT_PUBLIC_THEME_COLOR_DARK="#0a0a0a"
|
NEXT_PUBLIC_THEME_COLOR_DARK="#0a0a0a"
|
||||||
|
|
||||||
# AUTH
|
# AUTH
|
||||||
NEXT_PUBLIC_AUTH_PASSWORD=false
|
NEXT_PUBLIC_AUTH_PASSWORD=true
|
||||||
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
|
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
|
||||||
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||||
|
|
||||||
@@ -65,6 +65,3 @@ NEXT_PUBLIC_TEAM_NAVIGATION_STYLE=custom
|
|||||||
NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom
|
NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom
|
||||||
|
|
||||||
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
||||||
|
|
||||||
# Configure Medusa password secret for Keycloak users
|
|
||||||
MEDUSA_PASSWORD_SECRET=ODEwMGNiMmUtOGMxYS0xMWYwLWJlZDYtYTM3YzYyMWY0NGEzCg==
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
# SITE
|
# SITE
|
||||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
NEXT_PUBLIC_AUTH_PASSWORD=true
|
|
||||||
|
|
||||||
# SUPABASE DEVELOPMENT
|
# SUPABASE DEVELOPMENT
|
||||||
|
|
||||||
@@ -26,65 +25,14 @@ EMAIL_PORT=1025 # or 465 for SSL
|
|||||||
EMAIL_TLS=false
|
EMAIL_TLS=false
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
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
|
||||||
MEDUSA_BACKEND_URL=http://localhost:9000
|
MEDUSA_BACKEND_URL=http://localhost:9000
|
||||||
MEDUSA_BACKEND_PUBLIC_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
|
# MONTONIO
|
||||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||||
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
|
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
|
||||||
MONTONIO_API_URL=https://sandbox-stargate.montonio.com
|
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
|
||||||
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
|
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@
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
||||||
|
|
||||||
# SUPABASE
|
# SUPABASE
|
||||||
NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
# NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
|
# 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
|
# # MONTONIO
|
||||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||||
|
|||||||
10
.env.staging
10
.env.staging
@@ -6,10 +6,10 @@
|
|||||||
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
||||||
|
|
||||||
# SUPABASE
|
# SUPABASE
|
||||||
NEXT_PUBLIC_SUPABASE_URL=https://kaldvociniytdbbcxvqk.supabase.co
|
# NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImthbGR2b2Npbml5dGRiYmN4dnFrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTYzNjQ5OTYsImV4cCI6MjA3MTk0MDk5Nn0.eixihH2KGkJZolY9FiQDicJOo2kxvXrSe6gGUCrkLo0
|
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
|
||||||
|
|
||||||
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||||
|
|
||||||
# MONTONIO
|
# # MONTONIO
|
||||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -22,12 +22,12 @@ COPY . .
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN set -a \
|
RUN set -a \
|
||||||
&& . .env \
|
&& . .env \
|
||||||
&& . .env.production \
|
&& . .env.production \
|
||||||
&& . .env.staging \
|
&& . .env.staging \
|
||||||
&& set +a \
|
&& set +a \
|
||||||
&& node check-env.js \
|
&& node check-env.js \
|
||||||
&& pnpm build
|
&& pnpm build
|
||||||
|
|
||||||
|
|
||||||
# --- Stage 2: Runtime ---
|
# --- Stage 2: Runtime ---
|
||||||
@@ -41,13 +41,13 @@ COPY --from=builder /app ./
|
|||||||
RUN cp ".env.${APP_ENV}" .env.local
|
RUN cp ".env.${APP_ENV}" .env.local
|
||||||
|
|
||||||
RUN npm install -g pnpm@9 \
|
RUN npm install -g pnpm@9 \
|
||||||
&& pnpm install --prod --frozen-lockfile
|
&& pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# 🔍 Optional: Log key envs for debug
|
# 🔍 Optional: Log key envs for debug
|
||||||
RUN echo "📄 .env contents:" && cat .env.local \
|
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
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import { Button } from '@kit/ui/button';
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { authConfig, featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
import { featureFlagsConfig } from '@kit/shared/config';
|
||||||
|
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
|
||||||
|
|
||||||
const ModeToggle = dynamic(() =>
|
const ModeToggle = dynamic(() =>
|
||||||
import('@kit/ui/mode-toggle').then((mod) => ({
|
import('@kit/ui/mode-toggle').then((mod) => ({
|
||||||
@@ -72,13 +75,11 @@ function AuthButtons() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{authConfig.providers.password && (
|
<Button asChild className="text-xs md:text-sm" variant={'default'}>
|
||||||
<Button asChild className="text-xs md:text-sm" variant={'default'}>
|
<Link href={pathsConfig.auth.signUp}>
|
||||||
<Link href={pathsConfig.auth.signUp}>
|
<Trans i18nKey={'auth:signUp'} />
|
||||||
<Trans i18nKey={'auth:signUp'} />
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -2,10 +2,10 @@ import axios from 'axios';
|
|||||||
import { XMLParser } from 'fast-xml-parser';
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
||||||
import { IMedipostPublicMessageDataParsed, IUuringElement } from '~/lib/services/medipost.types';
|
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost.types';
|
||||||
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service';
|
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service';
|
||||||
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
|
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
|
||||||
import { createAnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
|
import { createAnalysisElement } from '~/lib/services/analysis-element.service';
|
||||||
import { createCodes } from '~/lib/services/codes.service';
|
import { createCodes } from '~/lib/services/codes.service';
|
||||||
import { getLatestPublicMessageListItem } from '~/lib/services/medipost.service';
|
import { getLatestPublicMessageListItem } from '~/lib/services/medipost.service';
|
||||||
import type { ICode } from '~/lib/types/code';
|
import type { ICode } from '~/lib/types/code';
|
||||||
@@ -80,92 +80,81 @@ export default async function syncAnalysisGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const codes: ICode[] = [];
|
const codes: ICode[] = [];
|
||||||
const analysesToCreate: { analysisGroupId: number, analyses: IUuringElement[], analysisElementId: number }[] = [];
|
|
||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
let analysisGroupId: number | undefined;
|
|
||||||
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
|
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
|
||||||
if (existingAnalysisGroup) {
|
if (existingAnalysisGroup) {
|
||||||
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`);
|
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`);
|
||||||
analysisGroupId = existingAnalysisGroup.id;
|
continue;
|
||||||
} 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);
|
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||||
|
|
||||||
for (const item of analysisGroupItems) {
|
for (const item of analysisGroupItems) {
|
||||||
const analysisElement = item.UuringuElement;
|
const analysisElement = item.UuringuElement;
|
||||||
|
|
||||||
let insertedAnalysisElementId: number | undefined;
|
const insertedAnalysisElementId = await createAnalysisElement({
|
||||||
const existingAnalysisElement = (await getAnalysisElements({ originalIds: [analysisElement.UuringId] }))?.[0];
|
analysisElement,
|
||||||
if (existingAnalysisElement) {
|
analysisGroupId,
|
||||||
console.info(`Analysis element '${analysisElement.UuringNimi}' already exists`);
|
materialGroups: toArray(item.MaterjalideGrupp),
|
||||||
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(
|
codes.push(
|
||||||
...analysisCodes.map((kood) => ({
|
...analysisElementCodes.map((kood) => ({
|
||||||
hk_code: kood.HkKood,
|
hk_code: kood.HkKood,
|
||||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||||
coefficient: kood.Koefitsient,
|
coefficient: kood.Koefitsient,
|
||||||
price: kood.Hind,
|
price: kood.Hind,
|
||||||
analysis_group_id: null,
|
analysis_group_id: null,
|
||||||
analysis_element_id: null,
|
analysis_element_id: insertedAnalysisElementId,
|
||||||
analysis_id: insertedAnalysisId,
|
analysis_id: null,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export const POST = async (request: NextRequest) => {
|
|||||||
console.error("Error syncing analysis groups", e);
|
console.error("Error syncing analysis groups", e);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: 'Failed to sync analysis groups',
|
message: 'Failed to sync analysis groups',
|
||||||
error: e instanceof Error ? JSON.stringify(e, undefined, 2) : 'Unknown error',
|
|
||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +1,19 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
import { createAuthCallbackService, getErrorURLParameters } from '@kit/supabase/auth';
|
import { createAuthCallbackService } from '@kit/supabase/auth';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
|
||||||
|
|
||||||
const ERROR_PATH = '/auth/callback/error';
|
|
||||||
|
|
||||||
const redirectOnError = (searchParams?: string) => {
|
|
||||||
return redirect(`${ERROR_PATH}${searchParams ? `?${searchParams}` : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
|
|
||||||
const error = searchParams.get('error');
|
|
||||||
if (error) {
|
|
||||||
const { searchParams } = getErrorURLParameters({ error });
|
|
||||||
return redirectOnError(searchParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
const authCode = searchParams.get('code');
|
|
||||||
if (!authCode) {
|
|
||||||
return redirectOnError();
|
|
||||||
}
|
|
||||||
|
|
||||||
let redirectPath = searchParams.get('next') || pathsConfig.app.home;
|
|
||||||
// if we have an invite token, we redirect to the join team page
|
|
||||||
// instead of the default next url. This is because the user is trying
|
|
||||||
// to join a team and we want to make sure they are redirected to the
|
|
||||||
// correct page.
|
|
||||||
const inviteToken = searchParams.get('invite_token');
|
|
||||||
if (inviteToken) {
|
|
||||||
const urlParams = new URLSearchParams({
|
|
||||||
invite_token: inviteToken,
|
|
||||||
email: searchParams.get('email') ?? '',
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectPath = `${pathsConfig.app.joinTeam}?${urlParams.toString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const service = createAuthCallbackService(getSupabaseServerClient());
|
const service = createAuthCallbackService(getSupabaseServerClient());
|
||||||
const oauthResult = await service.exchangeCodeForSession(authCode);
|
|
||||||
if (!("isSuccess" in oauthResult)) {
|
|
||||||
return redirectOnError(oauthResult.searchParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = createAccountsApi(getSupabaseServerClient());
|
const { nextPath } = await service.exchangeCodeForSession(request, {
|
||||||
|
joinTeamPath: pathsConfig.app.joinTeam,
|
||||||
|
redirectPath: pathsConfig.app.home,
|
||||||
|
});
|
||||||
|
|
||||||
const account = await api.getPersonalAccountByUserId(
|
return redirect(nextPath);
|
||||||
oauthResult.user.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!account.email || !account.name || !account.last_name) {
|
|
||||||
return redirect(pathsConfig.auth.updateAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(redirectPath);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { SignInMethodsContainer } from '@kit/auth/sign-in';
|
|
||||||
import { authConfig, pathsConfig } from '@kit/shared/config';
|
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import { Heading } from '@kit/ui/heading';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
export default function PasswordOption({
|
|
||||||
inviteToken,
|
|
||||||
returnPath,
|
|
||||||
}: {
|
|
||||||
inviteToken?: string;
|
|
||||||
returnPath?: string;
|
|
||||||
}) {
|
|
||||||
const signUpPath =
|
|
||||||
pathsConfig.auth.signUp +
|
|
||||||
(inviteToken ? `?invite_token=${inviteToken}` : '');
|
|
||||||
|
|
||||||
const paths = {
|
|
||||||
callback: pathsConfig.auth.callback,
|
|
||||||
returnPath: returnPath ?? pathsConfig.app.home,
|
|
||||||
joinTeam: pathsConfig.app.joinTeam,
|
|
||||||
updateAccount: pathsConfig.auth.updateAccount,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={'flex flex-col items-center gap-1'}>
|
|
||||||
<Heading level={4} className={'tracking-tight'}>
|
|
||||||
<Trans i18nKey={'auth:signInHeading'} />
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<p className={'text-muted-foreground text-sm'}>
|
|
||||||
<Trans i18nKey={'auth:signInSubheading'} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SignInMethodsContainer
|
|
||||||
inviteToken={inviteToken}
|
|
||||||
paths={paths}
|
|
||||||
providers={authConfig.providers}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={'flex justify-center'}>
|
|
||||||
<Button asChild variant={'link'} size={'sm'}>
|
|
||||||
<Link href={signUpPath} prefetch={true}>
|
|
||||||
<Trans i18nKey={'auth:doNotHaveAccountYet'} />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Loading from '@/app/home/loading';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { getSupabaseBrowserClient } from '@/packages/supabase/src/clients/browser-client';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
export function SignInPageClientRedirect() {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function signIn() {
|
|
||||||
const { data, error } = await getSupabaseBrowserClient()
|
|
||||||
.auth
|
|
||||||
.signInWithOAuth({
|
|
||||||
provider: 'keycloak',
|
|
||||||
options: {
|
|
||||||
redirectTo: `${window.location.origin}/auth/callback`,
|
|
||||||
queryParams: {
|
|
||||||
prompt: 'login',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('OAuth error', error);
|
|
||||||
router.push('/');
|
|
||||||
} else if (data.url) {
|
|
||||||
router.push(data.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signIn();
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { pathsConfig, authConfig } from '@kit/shared/config';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
|
||||||
|
import { SignInMethodsContainer } from '@kit/auth/sign-in';
|
||||||
|
import { authConfig, pathsConfig } from '@kit/shared/config';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { Heading } from '@kit/ui/heading';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import { SignInPageClientRedirect } from './components/SignInPageClientRedirect';
|
|
||||||
import PasswordOption from './components/PasswordOption';
|
|
||||||
|
|
||||||
interface SignInPageProps {
|
interface SignInPageProps {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
@@ -21,14 +26,47 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function SignInPage({ searchParams }: SignInPageProps) {
|
async function SignInPage({ searchParams }: SignInPageProps) {
|
||||||
const { invite_token: inviteToken, next: returnPath = pathsConfig.app.home } =
|
const { invite_token: inviteToken, next = pathsConfig.app.home } =
|
||||||
await searchParams;
|
await searchParams;
|
||||||
|
|
||||||
if (authConfig.providers.password) {
|
const signUpPath =
|
||||||
return <PasswordOption inviteToken={inviteToken} returnPath={returnPath} />;
|
pathsConfig.auth.signUp +
|
||||||
}
|
(inviteToken ? `?invite_token=${inviteToken}` : '');
|
||||||
|
|
||||||
return <SignInPageClientRedirect />;
|
const paths = {
|
||||||
|
callback: pathsConfig.auth.callback,
|
||||||
|
returnPath: next ?? pathsConfig.app.home,
|
||||||
|
joinTeam: pathsConfig.app.joinTeam,
|
||||||
|
updateAccount: pathsConfig.auth.updateAccount,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={'flex flex-col items-center gap-1'}>
|
||||||
|
<Heading level={4} className={'tracking-tight'}>
|
||||||
|
<Trans i18nKey={'auth:signInHeading'} />
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<p className={'text-muted-foreground text-sm'}>
|
||||||
|
<Trans i18nKey={'auth:signInSubheading'} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SignInMethodsContainer
|
||||||
|
inviteToken={inviteToken}
|
||||||
|
paths={paths}
|
||||||
|
providers={authConfig.providers}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={'flex justify-center'}>
|
||||||
|
<Button asChild variant={'link'} size={'sm'}>
|
||||||
|
<Link href={signUpPath} prefetch={true}>
|
||||||
|
<Trans i18nKey={'auth:doNotHaveAccountYet'} />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(SignInPage);
|
export default withI18n(SignInPage);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
|
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
|
||||||
import { authConfig, pathsConfig } from '@kit/shared/config';
|
import { authConfig, pathsConfig } from '@kit/shared/config';
|
||||||
@@ -38,10 +37,6 @@ async function SignUpPage({ searchParams }: Props) {
|
|||||||
pathsConfig.auth.signIn +
|
pathsConfig.auth.signIn +
|
||||||
(inviteToken ? `?invite_token=${inviteToken}` : '');
|
(inviteToken ? `?invite_token=${inviteToken}` : '');
|
||||||
|
|
||||||
if (!authConfig.providers.password) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'flex flex-col items-center gap-1'}>
|
<div className={'flex flex-col items-center gap-1'}>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { User } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { ExternalLink } from '@/public/assets/external-link';
|
import { ExternalLink } from '@/public/assets/external-link';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@@ -21,52 +23,31 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
|
|
||||||
import { UpdateAccountSchema } from '../_lib/schemas/update-account.schema';
|
import { UpdateAccountSchema } from '../_lib/schemas/update-account.schema';
|
||||||
import { onUpdateAccount } from '../_lib/server/update-account';
|
import { onUpdateAccount } from '../_lib/server/update-account';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
type UpdateAccountFormValues = z.infer<typeof UpdateAccountSchema>;
|
export function UpdateAccountForm({ user }: { user: User }) {
|
||||||
|
|
||||||
export function UpdateAccountForm({
|
|
||||||
defaultValues,
|
|
||||||
}: {
|
|
||||||
defaultValues: UpdateAccountFormValues,
|
|
||||||
}) {
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(UpdateAccountSchema),
|
resolver: zodResolver(UpdateAccountSchema),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues,
|
defaultValues: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
personalCode: '',
|
||||||
|
email: user.email,
|
||||||
|
phone: '',
|
||||||
|
city: '',
|
||||||
|
weight: 0,
|
||||||
|
height: 0,
|
||||||
|
userConsent: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { firstName, lastName, personalCode, email, weight, height, userConsent } = defaultValues;
|
|
||||||
|
|
||||||
const hasFirstName = !!firstName;
|
|
||||||
const hasLastName = !!lastName;
|
|
||||||
const hasPersonalCode = !!personalCode;
|
|
||||||
const hasEmail = !!email;
|
|
||||||
const hasWeight = !!weight;
|
|
||||||
const hasHeight = !!height;
|
|
||||||
const hasUserConsent = !!userConsent;
|
|
||||||
|
|
||||||
const onUpdateAccountOptions = async (values: UpdateAccountFormValues) =>
|
|
||||||
onUpdateAccount({
|
|
||||||
...values,
|
|
||||||
...(hasFirstName && { firstName }),
|
|
||||||
...(hasLastName && { lastName }),
|
|
||||||
...(hasPersonalCode && { personalCode }),
|
|
||||||
...(hasEmail && { email }),
|
|
||||||
...(hasWeight && { weight: values.weight ?? weight }),
|
|
||||||
...(hasHeight && { height: values.height ?? height }),
|
|
||||||
...(hasUserConsent && { userConsent: values.userConsent ?? userConsent }),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
className="flex flex-col gap-6 px-6 pt-10 text-left"
|
className="flex flex-col gap-6 px-6 pt-10 text-left"
|
||||||
onSubmit={form.handleSubmit(onUpdateAccountOptions)}
|
onSubmit={form.handleSubmit(onUpdateAccount)}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name="firstName"
|
name="firstName"
|
||||||
disabled={hasFirstName}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
@@ -82,7 +63,6 @@ export function UpdateAccountForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="lastName"
|
name="lastName"
|
||||||
disabled={hasLastName}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
@@ -98,7 +78,6 @@ export function UpdateAccountForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="personalCode"
|
name="personalCode"
|
||||||
disabled={hasPersonalCode}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
@@ -114,14 +93,13 @@ export function UpdateAccountForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="email"
|
name="email"
|
||||||
disabled={hasEmail}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'common:formField:email'} />
|
<Trans i18nKey={'common:formField:email'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} disabled />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import parsePhoneNumber from 'libphonenumber-js/min';
|
|
||||||
|
|
||||||
export const UpdateAccountSchema = z.object({
|
export const UpdateAccountSchema = z.object({
|
||||||
firstName: z
|
firstName: z
|
||||||
@@ -24,20 +23,7 @@ export const UpdateAccountSchema = z.object({
|
|||||||
.string({
|
.string({
|
||||||
error: 'Phone number is required',
|
error: 'Phone number is required',
|
||||||
})
|
})
|
||||||
.nonempty()
|
.nonempty(),
|
||||||
.refine(
|
|
||||||
(phone) => {
|
|
||||||
try {
|
|
||||||
const phoneNumber = parsePhoneNumber(phone);
|
|
||||||
return !!phoneNumber && phoneNumber.isValid() && phoneNumber.country === 'EE';
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'common:formFieldError.invalidPhoneNumber',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
weight: z
|
weight: z
|
||||||
.number({
|
.number({
|
||||||
|
|||||||
@@ -28,15 +28,11 @@ export const onUpdateAccount = enhanceAction(
|
|||||||
console.warn('On update account error: ', err);
|
console.warn('On update account error: ', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await updateCustomer({
|
||||||
await updateCustomer({
|
first_name: params.firstName,
|
||||||
first_name: params.firstName,
|
last_name: params.lastName,
|
||||||
last_name: params.lastName,
|
phone: params.phone,
|
||||||
phone: params.phone,
|
});
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to update Medusa customer", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasUnseenMembershipConfirmation =
|
const hasUnseenMembershipConfirmation =
|
||||||
await api.hasUnseenMembershipConfirmation();
|
await api.hasUnseenMembershipConfirmation();
|
||||||
|
|||||||
@@ -11,39 +11,18 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { UpdateAccountForm } from './_components/update-account-form';
|
import { UpdateAccountForm } from './_components/update-account-form';
|
||||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
|
||||||
import { toTitleCase } from '~/lib/utils';
|
|
||||||
|
|
||||||
async function UpdateAccount() {
|
async function UpdateAccount() {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { user },
|
data: { user },
|
||||||
} = await client.auth.getUser();
|
} = await client.auth.getUser();
|
||||||
const isKeycloakUser = user?.app_metadata?.provider === 'keycloak';
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(pathsConfig.auth.signIn);
|
redirect(pathsConfig.auth.signIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
firstName: account?.name ? toTitleCase(account.name) : '',
|
|
||||||
lastName: account?.last_name ? toTitleCase(account.last_name) : '',
|
|
||||||
personalCode: account?.personal_code ?? '',
|
|
||||||
email: (() => {
|
|
||||||
if (isKeycloakUser) {
|
|
||||||
return account?.email ?? '';
|
|
||||||
}
|
|
||||||
return account?.email ?? user?.email ?? '';
|
|
||||||
})(),
|
|
||||||
phone: account?.phone ?? '',
|
|
||||||
city: account?.city ?? '',
|
|
||||||
weight: account?.accountParams?.weight ?? 0,
|
|
||||||
height: account?.accountParams?.height ?? 0,
|
|
||||||
userConsent: account?.has_consent_personal_data ?? false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
|
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
|
||||||
<div className="relative flex min-w-md flex-col px-12 pt-7 pb-22 text-center md:w-1/2">
|
<div className="relative flex min-w-md flex-col px-12 pt-7 pb-22 text-center md:w-1/2">
|
||||||
@@ -55,7 +34,7 @@ async function UpdateAccount() {
|
|||||||
<p className="text-muted-foreground pt-1 text-sm">
|
<p className="text-muted-foreground pt-1 text-sm">
|
||||||
<Trans i18nKey={'account:updateAccount:description'} />
|
<Trans i18nKey={'account:updateAccount:description'} />
|
||||||
</p>
|
</p>
|
||||||
<UpdateAccountForm defaultValues={defaultValues} />
|
<UpdateAccountForm user={user} />
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat md:block"></div>
|
<div className="hidden w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat md:block"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function UserHomePage() {
|
|||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const account = await loadCurrentUserAccount();
|
const account = await loadCurrentUserAccount();
|
||||||
const api = createAccountsApi(client);
|
const api = await createAccountsApi(client);
|
||||||
const bmiThresholds = await api.fetchBmiThresholds();
|
const bmiThresholds = await api.fetchBmiThresholds();
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Badge, Heading, Text } from "@medusajs/ui"
|
import { Badge, Text } from "@medusajs/ui"
|
||||||
|
import { toast } from '@kit/ui/sonner';
|
||||||
import React, { useActionState } from "react";
|
import React, { useActionState } from "react";
|
||||||
|
|
||||||
import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
|
import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
|
||||||
@@ -31,11 +32,19 @@ export default function DiscountCode({ cart }: {
|
|||||||
|
|
||||||
const removePromotionCode = async (code: string) => {
|
const removePromotionCode = async (code: string) => {
|
||||||
const validPromotions = promotions.filter(
|
const validPromotions = promotions.filter(
|
||||||
(promotion) => promotion.code !== code
|
(promotion) => promotion.code !== code,
|
||||||
)
|
)
|
||||||
|
|
||||||
await applyPromotions(
|
await applyPromotions(
|
||||||
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!)
|
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('cart:discountCode.removeSuccess'));
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t('cart:discountCode.removeError'));
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +54,14 @@ export default function DiscountCode({ cart }: {
|
|||||||
.map((p) => p.code!)
|
.map((p) => p.code!)
|
||||||
codes.push(code.toString())
|
codes.push(code.toString())
|
||||||
|
|
||||||
await applyPromotions(codes)
|
await applyPromotions(codes, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('cart:discountCode.addSuccess'));
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t('cart:discountCode.addError'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
form.reset()
|
form.reset()
|
||||||
}
|
}
|
||||||
@@ -64,7 +80,7 @@ export default function DiscountCode({ cart }: {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
||||||
className="w-full mb-2 flex gap-x-2"
|
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name={'code'}
|
name={'code'}
|
||||||
@@ -87,16 +103,12 @@ export default function DiscountCode({ cart }: {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
{promotions.length > 0 ? (
|
||||||
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
<div className="w-full flex items-center mt-4">
|
||||||
</p>
|
<div className="flex flex-col w-full gap-y-2">
|
||||||
|
<p>
|
||||||
{promotions.length > 0 && (
|
<Trans i18nKey={'cart:discountCode.appliedCodes'} />
|
||||||
<div className="w-full flex items-center">
|
</p>
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<Heading className="txt-medium mb-2">
|
|
||||||
Promotion(s) applied:
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
{promotions.map((promotion) => {
|
{promotions.map((promotion) => {
|
||||||
return (
|
return (
|
||||||
@@ -110,6 +122,7 @@ export default function DiscountCode({ cart }: {
|
|||||||
<Badge
|
<Badge
|
||||||
color={promotion.is_automatic ? "green" : "grey"}
|
color={promotion.is_automatic ? "green" : "grey"}
|
||||||
size="small"
|
size="small"
|
||||||
|
className="px-4"
|
||||||
>
|
>
|
||||||
{promotion.code}
|
{promotion.code}
|
||||||
</Badge>{" "}
|
</Badge>{" "}
|
||||||
@@ -151,7 +164,7 @@ export default function DiscountCode({ cart }: {
|
|||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
Remove discount code from order
|
<Trans i18nKey={'cart:discountCode.remove'} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -160,6 +173,10 @@ export default function DiscountCode({ cart }: {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import { formatCurrency } from "@/packages/shared/src/utils";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { handleNavigateToPayment } from "@/lib/services/medusaCart.service";
|
import { handleNavigateToPayment } from "@/lib/services/medusaCart.service";
|
||||||
import AnalysisLocation from "./analysis-location";
|
import AnalysisLocation from "./analysis-location";
|
||||||
import { composeOrderXML, getOrderedAnalysisIds } from "~/lib/services/medipost.service";
|
|
||||||
|
|
||||||
const IS_DISCOUNT_SHOWN = false as boolean;
|
const IS_DISCOUNT_SHOWN = true as boolean;
|
||||||
|
|
||||||
export default function Cart({
|
export default function Cart({
|
||||||
cart,
|
cart,
|
||||||
@@ -70,7 +69,7 @@ export default function Cart({
|
|||||||
|
|
||||||
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
|
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
|
||||||
const isLocationsShown = synlabAnalyses.length > 0;
|
const isLocationsShown = synlabAnalyses.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4">
|
<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">
|
<div className="flex flex-col bg-white gap-y-6">
|
||||||
@@ -78,28 +77,62 @@ export default function Cart({
|
|||||||
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
|
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
|
||||||
</div>
|
</div>
|
||||||
{hasCartItems && (
|
{hasCartItems && (
|
||||||
<div className="flex justify-end gap-x-4 px-6 py-4">
|
<>
|
||||||
<div className="mr-[36px]">
|
<div className="flex justify-end gap-x-4 px-6 pt-4">
|
||||||
<p className="ml-0 font-bold text-sm">
|
<div className="mr-[36px]">
|
||||||
<Trans i18nKey="cart:total" />
|
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
<div className="mr-[116px]">
|
<div className="flex justify-end gap-x-4 px-6 py-2">
|
||||||
<p className="text-sm">
|
<div className="mr-[36px]">
|
||||||
{formatCurrency({
|
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||||
value: cart.total,
|
<Trans i18nKey="cart:promotionsTotal" />
|
||||||
currencyCode: cart.currency_code,
|
</p>
|
||||||
locale: language,
|
</div>
|
||||||
})}
|
<div className="mr-[116px]">
|
||||||
</p>
|
<p className="text-sm">
|
||||||
|
{formatCurrency({
|
||||||
|
value: cart.discount_total,
|
||||||
|
currencyCode: cart.currency_code,
|
||||||
|
locale: language,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 className="flex gap-y-6 py-8">
|
<div className="flex sm:flex-row flex-col gap-y-6 py-8 gap-x-4">
|
||||||
{IS_DISCOUNT_SHOWN && (
|
{IS_DISCOUNT_SHOWN && (
|
||||||
<Card
|
<Card
|
||||||
className="flex flex-col justify-between w-1/2"
|
className="flex flex-col justify-between w-full sm:w-1/2"
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
<h5>
|
<h5>
|
||||||
@@ -114,7 +147,7 @@ export default function Cart({
|
|||||||
|
|
||||||
{isLocationsShown && (
|
{isLocationsShown && (
|
||||||
<Card
|
<Card
|
||||||
className="flex flex-col justify-between w-1/2"
|
className="flex flex-col justify-between w-full sm:w-1/2"
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
<h5>
|
<h5>
|
||||||
@@ -134,26 +167,6 @@ export default function Cart({
|
|||||||
<Trans i18nKey="cart:checkout.goToCheckout" />
|
<Trans i18nKey="cart:checkout.goToCheckout" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const cards = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'dashboard:bmi',
|
title: 'dashboard:bmi',
|
||||||
description: bmiFromMetric(weight || 0, height || 0)?.toString() ?? '-',
|
description: bmiFromMetric(weight || 0, height || 0).toString(),
|
||||||
icon: <TrendingUp />,
|
icon: <TrendingUp />,
|
||||||
iconBg: getBmiBackgroundColor(bmiStatus),
|
iconBg: getBmiBackgroundColor(bmiStatus),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default function OrderAnalysesCards({
|
|||||||
className="px-2 text-black"
|
className="px-2 text-black"
|
||||||
onClick={() => handleSelect(variant.id)}
|
onClick={() => handleSelect(variant.id)}
|
||||||
>
|
>
|
||||||
{variantAddingToCart === variant.id ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
{variantAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ export const loadUserAccount = cache(accountLoader);
|
|||||||
|
|
||||||
export async function loadCurrentUserAccount() {
|
export async function loadCurrentUserAccount() {
|
||||||
const user = await requireUserInServerComponent();
|
const user = await requireUserInServerComponent();
|
||||||
return user?.id
|
return user?.identities?.[0]?.id
|
||||||
? await loadUserAccount(user.id)
|
? await loadUserAccount(user?.identities?.[0]?.id)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function accountLoader(userId: string) {
|
async function accountLoader(accountId: string) {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const api = createAccountsApi(client);
|
const api = createAccountsApi(client);
|
||||||
|
|
||||||
return api.getPersonalAccountByUserId(userId);
|
return api.getAccount(accountId);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,26 +3,9 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { createClient } from '@/utils/supabase/server';
|
import { createClient } from '@/utils/supabase/server';
|
||||||
import { medusaLogout } from '@lib/data/customer';
|
|
||||||
|
|
||||||
export const signOutAction = async () => {
|
export const signOutAction = async () => {
|
||||||
const client = await createClient();
|
const supabase = await createClient();
|
||||||
|
await supabase.auth.signOut();
|
||||||
try {
|
|
||||||
try {
|
|
||||||
await medusaLogout();
|
|
||||||
} catch (medusaError) {
|
|
||||||
console.warn('Medusa logout failed or not available:', medusaError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error } = await client.auth.signOut();
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Tables } from '@/packages/supabase/src/database.types';
|
|||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
import type { IUuringElement } from "./medipost.types";
|
import type { IUuringElement } from "./medipost.types";
|
||||||
|
|
||||||
export type AnalysesWithGroupsAndElements = ({
|
type AnalysesWithGroupsAndElements = ({
|
||||||
analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
||||||
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getClientInstitution,
|
getClientInstitution,
|
||||||
getClientPerson,
|
getClientPerson,
|
||||||
getConfidentiality,
|
getConfidentiality,
|
||||||
|
getOrderEnteredPerson,
|
||||||
getPais,
|
getPais,
|
||||||
getPatient,
|
getPatient,
|
||||||
getProviderInstitution,
|
getProviderInstitution,
|
||||||
@@ -37,10 +38,10 @@ 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, updateOrderStatus } from './order.service';
|
import { getOrder, updateOrderStatus } from './order.service';
|
||||||
import { AnalysisElement, getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
||||||
import { AnalysesWithGroupsAndElements, getAnalyses } from './analyses.service';
|
import { getAnalyses } from './analyses.service';
|
||||||
import { getAccountAdmin } from './account.service';
|
import { getAccountAdmin } from './account.service';
|
||||||
import { StoreOrder, StoreOrderLineItem } from '@medusajs/types';
|
import { StoreOrder } from '@medusajs/types';
|
||||||
import { listProducts } from '@lib/data/products';
|
import { listProducts } from '@lib/data/products';
|
||||||
import { listRegions } from '@lib/data/regions';
|
import { listRegions } from '@lib/data/regions';
|
||||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||||
@@ -480,60 +481,72 @@ export async function composeOrderXML({
|
|||||||
throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`);
|
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'>[] =
|
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
|
||||||
uniqBy(uniques, 'id');
|
uniqBy(
|
||||||
console.log('analysisGroups', { analysisGroups, uniques });
|
(
|
||||||
|
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ??
|
||||||
|
[]
|
||||||
|
).concat(
|
||||||
|
analyses?.flatMap(
|
||||||
|
({ analysis_elements }) => analysis_elements.analysis_groups,
|
||||||
|
) ?? [],
|
||||||
|
),
|
||||||
|
'id',
|
||||||
|
);
|
||||||
|
|
||||||
const specimenSection = [];
|
const specimenSection = [];
|
||||||
const analysisSection = [];
|
const analysisSection = [];
|
||||||
let order = 1;
|
let order = 1;
|
||||||
for (const currentGroup of analysisGroups) {
|
for (const currentGroup of analysisGroups) {
|
||||||
const relatedAnalysisElements = await getRelatedAnalysisElements({
|
let relatedAnalysisElement = analysisElements?.find(
|
||||||
analysisElements,
|
(element) => element.analysis_groups.id === currentGroup.id,
|
||||||
analyses,
|
);
|
||||||
currentGroup,
|
const relatedAnalyses = analyses?.filter((analysis) => {
|
||||||
|
return analysis.analysis_elements.analysis_groups.id === currentGroup.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const relatedAnalysisElement of relatedAnalysisElements) {
|
if (!relatedAnalysisElement) {
|
||||||
if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
|
relatedAnalysisElement = relatedAnalyses?.find(
|
||||||
throw new Error(
|
(relatedAnalysis) =>
|
||||||
`Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`,
|
relatedAnalysis.analysis_elements.analysis_groups.id ===
|
||||||
);
|
currentGroup.id,
|
||||||
}
|
)?.analysis_elements;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -541,14 +554,12 @@ export async function composeOrderXML({
|
|||||||
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
|
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
|
||||||
<Tellimus cito="EI">
|
<Tellimus cito="EI">
|
||||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||||
<!--<TellijaAsutus>-->
|
|
||||||
${getClientInstitution()}
|
${getClientInstitution()}
|
||||||
<!--<TeostajaAsutus>-->
|
|
||||||
${getProviderInstitution()}
|
${getProviderInstitution()}
|
||||||
<!--<TellijaIsik>-->
|
${getClientPerson()}
|
||||||
${getClientPerson(person)}
|
${getOrderEnteredPerson()}
|
||||||
<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
|
<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
|
||||||
${getPatient(person)}
|
${getPatient(person)}
|
||||||
${getConfidentiality()}
|
${getConfidentiality()}
|
||||||
${specimenSection.join('')}
|
${specimenSection.join('')}
|
||||||
${analysisSection?.join('')}
|
${analysisSection?.join('')}
|
||||||
@@ -683,7 +694,7 @@ async function syncPrivateMessage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: allOrderResponseElements } = await supabase
|
const { data: allOrderResponseElements} = await supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_response_elements')
|
.from('analysis_response_elements')
|
||||||
.select('*')
|
.select('*')
|
||||||
@@ -772,13 +783,10 @@ export async function sendOrderToMedipost({
|
|||||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderItems = {
|
|
||||||
items: Pick<StoreOrderLineItem, 'product'>[];
|
|
||||||
}
|
|
||||||
export async function getOrderedAnalysisIds({
|
export async function getOrderedAnalysisIds({
|
||||||
medusaOrder,
|
medusaOrder,
|
||||||
}: {
|
}: {
|
||||||
medusaOrder: OrderItems;
|
medusaOrder: StoreOrder;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
analysisElementId?: number;
|
analysisElementId?: number;
|
||||||
analysisId?: number;
|
analysisId?: number;
|
||||||
@@ -786,7 +794,7 @@ export async function getOrderedAnalysisIds({
|
|||||||
const countryCodes = await listRegions();
|
const countryCodes = await listRegions();
|
||||||
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
||||||
|
|
||||||
async function getOrderedAnalysisElements(medusaOrder: OrderItems) {
|
async function getOrderedAnalysisElements(medusaOrder: StoreOrder) {
|
||||||
const originalIds = (medusaOrder?.items ?? [])
|
const originalIds = (medusaOrder?.items ?? [])
|
||||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||||
.filter((a) => typeof a === 'string') as string[];
|
.filter((a) => typeof a === 'string') as string[];
|
||||||
@@ -794,7 +802,7 @@ export async function getOrderedAnalysisIds({
|
|||||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrderedAnalyses(medusaOrder: OrderItems) {
|
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
|
||||||
const originalIds = (medusaOrder?.items ?? [])
|
const originalIds = (medusaOrder?.items ?? [])
|
||||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||||
.filter((a) => typeof a === 'string') as string[];
|
.filter((a) => typeof a === 'string') as string[];
|
||||||
@@ -802,7 +810,7 @@ export async function getOrderedAnalysisIds({
|
|||||||
return analyses.map(({ id }) => ({ analysisId: id }));
|
return analyses.map(({ id }) => ({ analysisId: id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrderedAnalysisPackages(medusaOrder: OrderItems) {
|
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
|
||||||
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
||||||
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
||||||
if (orderedPackageIds.length === 0) {
|
if (orderedPackageIds.length === 0) {
|
||||||
@@ -859,10 +867,10 @@ export async function createMedipostActionLog({
|
|||||||
hasError = false,
|
hasError = false,
|
||||||
}: {
|
}: {
|
||||||
action:
|
action:
|
||||||
| 'send_order_to_medipost'
|
| 'send_order_to_medipost'
|
||||||
| 'sync_analysis_results_from_medipost'
|
| 'sync_analysis_results_from_medipost'
|
||||||
| 'send_fake_analysis_results_to_medipost'
|
| 'send_fake_analysis_results_to_medipost'
|
||||||
| 'send_analysis_results_to_medipost';
|
| 'send_analysis_results_to_medipost';
|
||||||
xml: string;
|
xml: string;
|
||||||
hasAnalysisResults?: boolean;
|
hasAnalysisResults?: boolean;
|
||||||
medusaOrderId?: string | null;
|
medusaOrderId?: string | null;
|
||||||
@@ -883,39 +891,3 @@ export async function createMedipostActionLog({
|
|||||||
.select('id')
|
.select('id')
|
||||||
.throwOnError();
|
.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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
getClientInstitution,
|
getClientInstitution,
|
||||||
getClientPerson,
|
getClientPerson,
|
||||||
|
getOrderEnteredPerson,
|
||||||
getPais,
|
getPais,
|
||||||
getPatient,
|
getPatient,
|
||||||
getProviderInstitution,
|
getProviderInstitution,
|
||||||
@@ -104,7 +105,8 @@ export async function composeOrderTestResponseXML({
|
|||||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||||
${getClientInstitution({ index: 1 })}
|
${getClientInstitution({ index: 1 })}
|
||||||
${getProviderInstitution({ index: 1 })}
|
${getProviderInstitution({ index: 1 })}
|
||||||
${getClientPerson(person)}
|
${getClientPerson()}
|
||||||
|
${getOrderEnteredPerson()}
|
||||||
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
|
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
|
||||||
|
|
||||||
${getPatient(person)}
|
${getPatient(person)}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export async function handleNavigateToPayment({
|
|||||||
const paymentLink =
|
const paymentLink =
|
||||||
await new MontonioOrderHandlerService().getMontonioPaymentLink({
|
await new MontonioOrderHandlerService().getMontonioPaymentLink({
|
||||||
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
|
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
|
||||||
returnUrl: `${"https://webhook.site"}/home/cart/montonio-callback`,
|
returnUrl: `${env().siteUrl}/home/cart/montonio-callback`,
|
||||||
amount: cart.total,
|
amount: cart.total,
|
||||||
currency: cart.currency_code.toUpperCase(),
|
currency: cart.currency_code.toUpperCase(),
|
||||||
description: `Order from Medreport`,
|
description: `Order from Medreport`,
|
||||||
|
|||||||
@@ -21,70 +21,48 @@ export const getPais = (
|
|||||||
<Saaja>${recipient}</Saaja>
|
<Saaja>${recipient}</Saaja>
|
||||||
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
|
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
|
||||||
<SaadetisId>${orderId}</SaadetisId>
|
<SaadetisId>${orderId}</SaadetisId>
|
||||||
<Email>argo@medreport.ee</Email>
|
<Email>info@medreport.ee</Email>
|
||||||
</Pais>`;
|
</Pais>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClientInstitution = ({ index }: { index?: number } = {}) => {
|
export const getClientInstitution = ({ index }: { index?: number } = {}) => {
|
||||||
if (isProd) {
|
|
||||||
// return correct data
|
|
||||||
}
|
|
||||||
return `<Asutus tyyp="TELLIJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
return `<Asutus tyyp="TELLIJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
||||||
<AsutuseId>16381793</AsutuseId>
|
<AsutuseId>16381793</AsutuseId>
|
||||||
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
|
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
|
||||||
<AsutuseKood>TSU</AsutuseKood>
|
<AsutuseKood>MRP</AsutuseKood>
|
||||||
<Telefon>+37258871517</Telefon>
|
<Telefon>+37258871517</Telefon>
|
||||||
</Asutus>`;
|
</Asutus>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProviderInstitution = ({ index }: { index?: number } = {}) => {
|
export const getProviderInstitution = ({ index }: { index?: number } = {}) => {
|
||||||
if (isProd) {
|
|
||||||
// return correct data
|
|
||||||
}
|
|
||||||
return `<Asutus tyyp="TEOSTAJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
return `<Asutus tyyp="TEOSTAJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
||||||
<AsutuseId>11107913</AsutuseId>
|
<AsutuseId>11107913</AsutuseId>
|
||||||
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
|
<AsutuseNimi>Synlab Eesti OÜ</AsutuseNimi>
|
||||||
<AsutuseKood>SLA</AsutuseKood>
|
<AsutuseKood>HTI</AsutuseKood>
|
||||||
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
|
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
|
||||||
<Telefon>+3723417123</Telefon>
|
<Telefon>+37217123</Telefon>
|
||||||
</Asutus>`;
|
</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">
|
return `<Personal tyyp="TELLIJA" jarjenumber="1">
|
||||||
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||||
<PersonalKood>${idCode}</PersonalKood>
|
<PersonalKood>D07907</PersonalKood>
|
||||||
<PersonalPerekonnaNimi>${lastName}</PersonalPerekonnaNimi>
|
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||||
<PersonalEesNimi>${firstName}</PersonalEesNimi>
|
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||||
${phone ? `<Telefon>${phone.startsWith('+372') ? phone : `+372${phone}`}</Telefon>` : ''}
|
<Telefon>+37258131202</Telefon>
|
||||||
</Personal>`;
|
</Personal>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const getOrderEnteredPerson = () => {
|
export const getOrderEnteredPerson = () => {
|
||||||
// if (isProd) {
|
return `<Personal tyyp="SISESTAJA" jarjenumber="2">
|
||||||
// // return correct data
|
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||||
// }
|
<PersonalKood>D07907</PersonalKood>
|
||||||
// return `<Personal tyyp="SISESTAJA" jarjenumber="1">
|
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||||
// <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||||
// <PersonalKood>D07907</PersonalKood>
|
<Telefon>+37258131202</Telefon>
|
||||||
// <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
</Personal>`;
|
||||||
// <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
};
|
||||||
// <Telefon>+37258131202</Telefon>
|
|
||||||
// </Personal>`;
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const getPatient = ({
|
export const getPatient = ({
|
||||||
idCode,
|
idCode,
|
||||||
|
|||||||
23
lib/utils.ts
23
lib/utils.ts
@@ -15,12 +15,11 @@ export function toArray<T>(input?: T | T[] | null): T[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toTitleCase(str?: string) {
|
export function toTitleCase(str?: string) {
|
||||||
return (
|
if (!str) return '';
|
||||||
str
|
return str.replace(
|
||||||
?.toLowerCase()
|
/\w\S*/g,
|
||||||
.replace(/[^-'’\s]+/g, (match) =>
|
(text: string) =>
|
||||||
match.replace(/^./, (first) => first.toUpperCase()),
|
text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
|
||||||
) ?? ""
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,12 +40,8 @@ export function sortByDate<T>(
|
|||||||
|
|
||||||
export const bmiFromMetric = (kg: number, cm: number) => {
|
export const bmiFromMetric = (kg: number, cm: number) => {
|
||||||
const m = cm / 100;
|
const m = cm / 100;
|
||||||
const m2 = m * m;
|
const bmi = kg / (m * m);
|
||||||
if (m2 === 0) {
|
return bmi ? Math.round(bmi) : NaN;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const bmi = kg / m2;
|
|
||||||
return !Number.isNaN(bmi) ? Math.round(bmi) : null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getBmiStatus(
|
export function getBmiStatus(
|
||||||
@@ -63,9 +58,7 @@ export function getBmiStatus(
|
|||||||
) || null;
|
) || null;
|
||||||
const bmi = bmiFromMetric(params.weight, params.height);
|
const bmi = bmiFromMetric(params.weight, params.height);
|
||||||
|
|
||||||
if (!thresholdByAge || bmi === null) {
|
if (!thresholdByAge || Number.isNaN(bmi)) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bmi > thresholdByAge.obesity_min) return BmiCategory.OBESE;
|
if (bmi > thresholdByAge.obesity_min) return BmiCategory.OBESE;
|
||||||
if (bmi > thresholdByAge.strong_min) return BmiCategory.VERY_OVERWEIGHT;
|
if (bmi > thresholdByAge.strong_min) return BmiCategory.VERY_OVERWEIGHT;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
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(', '));
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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'" }'
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
# 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
@@ -1,138 +0,0 @@
|
|||||||
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'
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
<?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
|
|
||||||
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>
|
|
||||||
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>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<?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
@@ -50,7 +50,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
mdxRs: true,
|
mdxRs: true,
|
||||||
reactCompiler: ENABLE_REACT_COMPILER,
|
reactCompiler: false,
|
||||||
optimizePackageImports: [
|
optimizePackageImports: [
|
||||||
'recharts',
|
'recharts',
|
||||||
'lucide-react',
|
'lucide-react',
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -69,7 +69,6 @@
|
|||||||
"fast-xml-parser": "^5.2.5",
|
"fast-xml-parser": "^5.2.5",
|
||||||
"isikukood": "3.1.7",
|
"isikukood": "3.1.7",
|
||||||
"jsonwebtoken": "9.0.2",
|
"jsonwebtoken": "9.0.2",
|
||||||
"libphonenumber-js": "^1.12.15",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
|
|||||||
@@ -79,9 +79,15 @@ export function PersonalAccountDropdown({
|
|||||||
}) {
|
}) {
|
||||||
const { data: personalAccountData } = usePersonalAccountData(user.id);
|
const { data: personalAccountData } = usePersonalAccountData(user.id);
|
||||||
|
|
||||||
const { name, last_name } = personalAccountData ?? {};
|
const signedInAsLabel = useMemo(() => {
|
||||||
const firstNameLabel = toTitleCase(name) ?? '-';
|
const email = user?.email ?? undefined;
|
||||||
const fullNameLabel = name && last_name ? toTitleCase(`${name} ${last_name}`) : '-';
|
const phone = user?.phone ?? undefined;
|
||||||
|
|
||||||
|
return email ?? phone;
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const displayName =
|
||||||
|
personalAccountData?.name ?? account?.name ?? user?.email ?? '';
|
||||||
|
|
||||||
const hasTotpFactor = useMemo(() => {
|
const hasTotpFactor = useMemo(() => {
|
||||||
const factors = user?.factors ?? [];
|
const factors = user?.factors ?? [];
|
||||||
@@ -122,7 +128,7 @@ export function PersonalAccountDropdown({
|
|||||||
<ProfileAvatar
|
<ProfileAvatar
|
||||||
className={'rounded-md'}
|
className={'rounded-md'}
|
||||||
fallbackClassName={'rounded-md border'}
|
fallbackClassName={'rounded-md border'}
|
||||||
displayName={firstNameLabel}
|
displayName={displayName ?? user?.email ?? ''}
|
||||||
pictureUrl={personalAccountData?.picture_url}
|
pictureUrl={personalAccountData?.picture_url}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -136,7 +142,7 @@ export function PersonalAccountDropdown({
|
|||||||
data-test={'account-dropdown-display-name'}
|
data-test={'account-dropdown-display-name'}
|
||||||
className={'truncate text-sm'}
|
className={'truncate text-sm'}
|
||||||
>
|
>
|
||||||
{firstNameLabel}
|
{toTitleCase(displayName)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -158,7 +164,7 @@ export function PersonalAccountDropdown({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className={'block truncate'}>{fullNameLabel}</span>
|
<span className={'block truncate'}>{signedInAsLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -48,41 +48,6 @@ class AccountsApi {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @name getPersonalAccountByUserId
|
|
||||||
* @description Get the personal account data for the given user ID.
|
|
||||||
* @param userId
|
|
||||||
*/
|
|
||||||
async getPersonalAccountByUserId(userId: string): Promise<AccountWithParams> {
|
|
||||||
const { data, error } = await this.client
|
|
||||||
.schema('medreport')
|
|
||||||
.from('accounts')
|
|
||||||
.select(
|
|
||||||
'*, accountParams: account_params (weight, height, isSmoker:is_smoker)',
|
|
||||||
)
|
|
||||||
.eq('primary_owner_user_id', userId)
|
|
||||||
.eq('is_personal_account', true)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { personal_code, ...rest } = data;
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
personal_code: (() => {
|
|
||||||
if (!personal_code) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (personal_code.toLowerCase().startsWith('ee')) {
|
|
||||||
return personal_code.substring(2);
|
|
||||||
}
|
|
||||||
return personal_code;
|
|
||||||
})(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name getAccountWorkspace
|
* @name getAccountWorkspace
|
||||||
* @description Get the account workspace data.
|
* @description Get the account workspace data.
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import { AuthProviderButton } from './auth-provider-button';
|
|||||||
* @see https://supabase.com/docs/guides/auth/social-login
|
* @see https://supabase.com/docs/guides/auth/social-login
|
||||||
*/
|
*/
|
||||||
const OAUTH_SCOPES: Partial<Record<Provider, string>> = {
|
const OAUTH_SCOPES: Partial<Record<Provider, string>> = {
|
||||||
// azure: 'email',
|
azure: 'email',
|
||||||
// keycloak: 'openid',
|
keycloak: 'openid',
|
||||||
// add your OAuth providers here
|
// add your OAuth providers here
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,12 +88,10 @@ export const OauthProviders: React.FC<{
|
|||||||
queryParams.set('invite_token', props.inviteToken);
|
queryParams.set('invite_token', props.inviteToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// signicat/keycloak will not allow redirect-uri with changing query params
|
const redirectPath = [
|
||||||
const INCLUDE_QUERY_PARAMS = false as boolean;
|
props.paths.callback,
|
||||||
|
queryParams.toString(),
|
||||||
const redirectPath = INCLUDE_QUERY_PARAMS
|
].join('?');
|
||||||
? [props.paths.callback, queryParams.toString()].join('?')
|
|
||||||
: props.paths.callback;
|
|
||||||
|
|
||||||
const redirectTo = [origin, redirectPath].join('');
|
const redirectTo = [origin, redirectPath].join('');
|
||||||
const scopes = OAUTH_SCOPES[provider] ?? undefined;
|
const scopes = OAUTH_SCOPES[provider] ?? undefined;
|
||||||
@@ -104,7 +102,6 @@ export const OauthProviders: React.FC<{
|
|||||||
redirectTo,
|
redirectTo,
|
||||||
queryParams: props.queryParams,
|
queryParams: props.queryParams,
|
||||||
scopes,
|
scopes,
|
||||||
// skipBrowserRedirect: false,
|
|
||||||
},
|
},
|
||||||
} satisfies SignInWithOAuthCredentials;
|
} satisfies SignInWithOAuthCredentials;
|
||||||
|
|
||||||
|
|||||||
@@ -108,9 +108,6 @@ export function SignInMethodsContainer(props: {
|
|||||||
callback: props.paths.callback,
|
callback: props.paths.callback,
|
||||||
returnPath: props.paths.returnPath,
|
returnPath: props.paths.returnPath,
|
||||||
}}
|
}}
|
||||||
queryParams={{
|
|
||||||
prompt: 'login',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function SignUpMethodsContainer(props: {
|
|||||||
emailRedirectTo={props.paths.callback}
|
emailRedirectTo={props.paths.callback}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
displayTermsCheckbox={props.displayTermsCheckbox}
|
displayTermsCheckbox={props.displayTermsCheckbox}
|
||||||
//onSignUp={() => redirect(redirectUrl)}
|
onSignUp={() => redirect(redirectUrl)}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
@@ -79,9 +79,6 @@ export function SignUpMethodsContainer(props: {
|
|||||||
callback: props.paths.callback,
|
callback: props.paths.callback,
|
||||||
returnPath: props.paths.appHome,
|
returnPath: props.paths.appHome,
|
||||||
}}
|
}}
|
||||||
queryParams={{
|
|
||||||
prompt: 'login',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ export const OrderSchema = z.object({
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
isPackage: z.boolean(),
|
isPackage: z.boolean(),
|
||||||
analysisOrderId: z.number(),
|
analysisOrderId: z.number(),
|
||||||
productMetadata: z.object({
|
|
||||||
analysisIdOriginal: z.string().nullable(),
|
|
||||||
analysisResultUnit: z.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
export type Order = z.infer<typeof OrderSchema>;
|
export type Order = z.infer<typeof OrderSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ export async function getOrSetCart(countryCode: string) {
|
|||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCart({ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string }) {
|
export async function updateCart(
|
||||||
|
{ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string },
|
||||||
|
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
|
||||||
|
) {
|
||||||
const cartId = id || (await getCartId());
|
const cartId = id || (await getCartId());
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
@@ -109,9 +112,13 @@ export async function updateCart({ id, ...data }: HttpTypes.StoreUpdateCart & {
|
|||||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||||
revalidateTag(fulfillmentCacheTag);
|
revalidateTag(fulfillmentCacheTag);
|
||||||
|
|
||||||
|
onSuccess();
|
||||||
return cart;
|
return cart;
|
||||||
})
|
})
|
||||||
.catch(medusaError);
|
.catch((e) => {
|
||||||
|
onError();
|
||||||
|
return medusaError(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addToCart({
|
export async function addToCart({
|
||||||
@@ -259,7 +266,10 @@ export async function initiatePaymentSession(
|
|||||||
.catch(medusaError);
|
.catch(medusaError);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyPromotions(codes: string[]) {
|
export async function applyPromotions(
|
||||||
|
codes: string[],
|
||||||
|
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
|
||||||
|
) {
|
||||||
const cartId = await getCartId();
|
const cartId = await getCartId();
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
@@ -278,8 +288,13 @@ export async function applyPromotions(codes: string[]) {
|
|||||||
|
|
||||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||||
revalidateTag(fulfillmentCacheTag);
|
revalidateTag(fulfillmentCacheTag);
|
||||||
|
|
||||||
|
onSuccess();
|
||||||
})
|
})
|
||||||
.catch(medusaError);
|
.catch((e) => {
|
||||||
|
onError();
|
||||||
|
return medusaError(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyGiftCard(code: string) {
|
export async function applyGiftCard(code: string) {
|
||||||
@@ -427,7 +442,7 @@ export async function placeOrder(cartId?: string, options: { revalidateCacheTags
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("Cart is not an order");
|
throw new Error("Cart is not an order");
|
||||||
}
|
}
|
||||||
|
|
||||||
return retrieveOrder(cartRes.order.id);
|
return retrieveOrder(cartRes.order.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { sdk } from "@lib/config"
|
|||||||
import medusaError from "@lib/util/medusa-error"
|
import medusaError from "@lib/util/medusa-error"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { revalidateTag } from "next/cache"
|
import { revalidateTag } from "next/cache"
|
||||||
|
import { redirect } from "next/navigation"
|
||||||
import {
|
import {
|
||||||
getAuthHeaders,
|
getAuthHeaders,
|
||||||
getCacheOptions,
|
getCacheOptions,
|
||||||
@@ -126,7 +127,7 @@ export async function login(_currentState: unknown, formData: FormData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function medusaLogout(countryCode = 'ee') {
|
export async function signout(countryCode?: string, shouldRedirect = true) {
|
||||||
await sdk.auth.logout()
|
await sdk.auth.logout()
|
||||||
|
|
||||||
await removeAuthToken()
|
await removeAuthToken()
|
||||||
@@ -138,6 +139,10 @@ export async function medusaLogout(countryCode = 'ee') {
|
|||||||
|
|
||||||
const cartCacheTag = await getCacheTag("carts")
|
const cartCacheTag = await getCacheTag("carts")
|
||||||
revalidateTag(cartCacheTag)
|
revalidateTag(cartCacheTag)
|
||||||
|
|
||||||
|
if (shouldRedirect) {
|
||||||
|
redirect(`/${countryCode!}/account`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function transferCart() {
|
export async function transferCart() {
|
||||||
@@ -257,110 +262,72 @@ export const updateCustomerAddress = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function medusaLogin(email: string, password: string) {
|
|
||||||
const token = await sdk.auth.login("customer", "emailpass", { email, password });
|
|
||||||
await setAuthToken(token as string);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await transferCart();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to transfer cart", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const customer = await retrieveCustomer();
|
|
||||||
if (!customer) {
|
|
||||||
throw new Error("Customer not found for active session");
|
|
||||||
}
|
|
||||||
|
|
||||||
return customer.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function medusaRegister({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
name,
|
|
||||||
lastName,
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
name: string | undefined;
|
|
||||||
lastName: string | undefined;
|
|
||||||
}) {
|
|
||||||
console.info(`Creating new Medusa account for Keycloak user with email=${email}`);
|
|
||||||
|
|
||||||
const registerToken = await sdk.auth.register("customer", "emailpass", { email, password });
|
|
||||||
await setAuthToken(registerToken);
|
|
||||||
|
|
||||||
console.info(`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`);
|
|
||||||
await sdk.store.customer.create(
|
|
||||||
{ email, first_name: name, last_name: lastName },
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
...(await getAuthHeaders()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function medusaLoginOrRegister(credentials: {
|
export async function medusaLoginOrRegister(credentials: {
|
||||||
email: string
|
email: string
|
||||||
supabaseUserId?: string
|
password?: string
|
||||||
name?: string,
|
}) {
|
||||||
lastName?: string,
|
const { email, password } = credentials;
|
||||||
} & ({ isDevPasswordLogin: true; password: string } | { isDevPasswordLogin?: false; password?: undefined })) {
|
|
||||||
const { email, supabaseUserId, name, lastName } = credentials;
|
|
||||||
|
|
||||||
|
|
||||||
const password = await (async () => {
|
|
||||||
if (credentials.isDevPasswordLogin) {
|
|
||||||
return credentials.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
return generateDeterministicPassword(email, supabaseUserId);
|
|
||||||
})();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await medusaLogin(email, password);
|
const token = await sdk.auth.login("customer", "emailpass", {
|
||||||
} catch (loginError) {
|
email,
|
||||||
console.error("Failed to login customer, attempting to register", loginError);
|
password,
|
||||||
|
});
|
||||||
|
await setAuthToken(token as string);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await medusaRegister({ email, password, name, lastName });
|
await transferCart();
|
||||||
return await medusaLogin(email, password);
|
} catch (e) {
|
||||||
|
console.error("Failed to transfer cart", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerCacheTag = await getCacheTag("customers");
|
||||||
|
revalidateTag(customerCacheTag);
|
||||||
|
|
||||||
|
const customer = await retrieveCustomer();
|
||||||
|
if (!customer) {
|
||||||
|
throw new Error("Customer not found");
|
||||||
|
}
|
||||||
|
return customer.id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to login customer, attempting to register", error);
|
||||||
|
try {
|
||||||
|
const registerToken = await sdk.auth.register("customer", "emailpass", {
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
})
|
||||||
|
|
||||||
|
await setAuthToken(registerToken as string);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
await sdk.store.customer.create({ email }, {}, headers);
|
||||||
|
|
||||||
|
const loginToken = await sdk.auth.login("customer", "emailpass", {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
await setAuthToken(loginToken as string);
|
||||||
|
|
||||||
|
const customerCacheTag = await getCacheTag("customers");
|
||||||
|
revalidateTag(customerCacheTag);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transferCart();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to transfer cart", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customer = await retrieveCustomer();
|
||||||
|
if (!customer) {
|
||||||
|
throw new Error("Customer not found");
|
||||||
|
}
|
||||||
|
return customer.id;
|
||||||
} catch (registerError) {
|
} catch (registerError) {
|
||||||
console.error("Failed to create Medusa account for user with email=${email}", registerError);
|
|
||||||
throw medusaError(registerError);
|
throw medusaError(registerError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a deterministic password based on user identifier
|
|
||||||
* This ensures the same user always gets the same password for Medusa
|
|
||||||
*/
|
|
||||||
async function generateDeterministicPassword(email: string, userId?: string): Promise<string> {
|
|
||||||
// Use the user ID or email as the base for deterministic generation
|
|
||||||
const baseString = userId || email;
|
|
||||||
const secret = process.env.MEDUSA_PASSWORD_SECRET!;
|
|
||||||
|
|
||||||
// Create a deterministic password using HMAC
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const keyData = encoder.encode(secret);
|
|
||||||
const messageData = encoder.encode(baseString);
|
|
||||||
|
|
||||||
// Import key for HMAC
|
|
||||||
const key = await crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
keyData,
|
|
||||||
{ name: 'HMAC', hash: 'SHA-256' },
|
|
||||||
false,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
// Generate HMAC
|
|
||||||
const signature = await crypto.subtle.sign('HMAC', key, messageData);
|
|
||||||
// Convert to base64 and make it a valid password
|
|
||||||
const hashArray = Array.from(new Uint8Array(signature));
|
|
||||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
// Take first 24 characters and add some complexity
|
|
||||||
const basePassword = hashHex.substring(0, 24);
|
|
||||||
// Add some required complexity for Medusa (uppercase, lowercase, numbers, symbols)
|
|
||||||
return `Mk${basePassword}9!`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const listProducts = async ({
|
|||||||
},
|
},
|
||||||
headers,
|
headers,
|
||||||
next,
|
next,
|
||||||
//cache: "force-cache",
|
cache: "force-cache",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(({ products, count }) => {
|
.then(({ products, count }) => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import MapPin from "@modules/common/icons/map-pin"
|
|||||||
import Package from "@modules/common/icons/package"
|
import Package from "@modules/common/icons/package"
|
||||||
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { medusaLogout } from "@lib/data/customer"
|
import { signout } from "@lib/data/customer"
|
||||||
|
|
||||||
const AccountNav = ({
|
const AccountNav = ({
|
||||||
customer,
|
customer,
|
||||||
@@ -21,7 +21,7 @@ const AccountNav = ({
|
|||||||
const { countryCode } = useParams() as { countryCode: string }
|
const { countryCode } = useParams() as { countryCode: string }
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await medusaLogout(countryCode)
|
await signout(countryCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ 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 { ApplicationRole } from '@kit/accounts/types/accounts';
|
||||||
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
|
||||||
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';
|
||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
home: pathsConfig.app.home,
|
home: pathsConfig.app.home,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const authConfig = AuthConfigSchema.parse({
|
|||||||
providers: {
|
providers: {
|
||||||
password: process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true',
|
password: process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true',
|
||||||
magicLink: process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true',
|
magicLink: process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true',
|
||||||
oAuth: ['keycloak'],
|
oAuth: ['google'],
|
||||||
},
|
},
|
||||||
} satisfies z.infer<typeof AuthConfigSchema>);
|
} satisfies z.infer<typeof AuthConfigSchema>);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
AuthError,
|
AuthError,
|
||||||
type EmailOtpType,
|
type EmailOtpType,
|
||||||
SupabaseClient,
|
SupabaseClient,
|
||||||
User,
|
|
||||||
} from '@supabase/supabase-js';
|
} from '@supabase/supabase-js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +20,7 @@ export function createAuthCallbackService(client: SupabaseClient) {
|
|||||||
* @description Service for handling auth callbacks in Supabase
|
* @description Service for handling auth callbacks in Supabase
|
||||||
*/
|
*/
|
||||||
class AuthCallbackService {
|
class AuthCallbackService {
|
||||||
constructor(private readonly client: SupabaseClient) { }
|
constructor(private readonly client: SupabaseClient) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name verifyTokenHash
|
* @name verifyTokenHash
|
||||||
@@ -129,117 +128,89 @@ class AuthCallbackService {
|
|||||||
/**
|
/**
|
||||||
* @name exchangeCodeForSession
|
* @name exchangeCodeForSession
|
||||||
* @description Exchanges the auth code for a session and redirects the user to the next page or an error page
|
* @description Exchanges the auth code for a session and redirects the user to the next page or an error page
|
||||||
* @param authCode
|
* @param request
|
||||||
|
* @param params
|
||||||
*/
|
*/
|
||||||
async exchangeCodeForSession(authCode: string): Promise<{
|
async exchangeCodeForSession(
|
||||||
isSuccess: boolean;
|
request: Request,
|
||||||
user: User;
|
params: {
|
||||||
} | ErrorURLParameters> {
|
joinTeamPath: string;
|
||||||
let user: User;
|
redirectPath: string;
|
||||||
try {
|
errorPath?: string;
|
||||||
const { data, error } =
|
},
|
||||||
await this.client.auth.exchangeCodeForSession(authCode);
|
): Promise<{
|
||||||
|
nextPath: string;
|
||||||
|
}> {
|
||||||
|
const requestUrl = new URL(request.url);
|
||||||
|
const searchParams = requestUrl.searchParams;
|
||||||
|
|
||||||
// if we have an error, we redirect to the error page
|
const authCode = searchParams.get('code');
|
||||||
if (error) {
|
const error = searchParams.get('error');
|
||||||
return getErrorURLParameters({
|
const nextUrlPathFromParams = searchParams.get('next');
|
||||||
code: error.code,
|
const inviteToken = searchParams.get('invite_token');
|
||||||
error: error.message,
|
const errorPath = params.errorPath ?? '/auth/callback/error';
|
||||||
|
|
||||||
|
let nextUrl = nextUrlPathFromParams ?? params.redirectPath;
|
||||||
|
|
||||||
|
// if we have an invite token, we redirect to the join team page
|
||||||
|
// instead of the default next url. This is because the user is trying
|
||||||
|
// to join a team and we want to make sure they are redirected to the
|
||||||
|
// correct page.
|
||||||
|
if (inviteToken) {
|
||||||
|
const emailParam = searchParams.get('email');
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams({
|
||||||
|
invite_token: inviteToken,
|
||||||
|
email: emailParam ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
nextUrl = `${params.joinTeamPath}?${urlParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authCode) {
|
||||||
|
try {
|
||||||
|
const { error } =
|
||||||
|
await this.client.auth.exchangeCodeForSession(authCode);
|
||||||
|
|
||||||
|
// if we have an error, we redirect to the error page
|
||||||
|
if (error) {
|
||||||
|
return onError({
|
||||||
|
code: error.code,
|
||||||
|
error: error.message,
|
||||||
|
path: errorPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
{
|
||||||
|
error,
|
||||||
|
name: `auth.callback`,
|
||||||
|
},
|
||||||
|
`An error occurred while exchanging code for session`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = error instanceof Error ? error.message : error;
|
||||||
|
|
||||||
|
return onError({
|
||||||
|
code: (error as AuthError)?.code,
|
||||||
|
error: message as string,
|
||||||
|
path: errorPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Keycloak users - set up Medusa integration
|
if (error) {
|
||||||
if (data?.user && this.isKeycloakUser(data.user)) {
|
return onError({
|
||||||
await this.setupMedusaUserForKeycloak(data.user);
|
error,
|
||||||
}
|
path: errorPath,
|
||||||
|
|
||||||
user = data.user;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
{
|
|
||||||
error,
|
|
||||||
name: `auth.callback`,
|
|
||||||
},
|
|
||||||
`An error occurred while exchanging code for session`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : error;
|
|
||||||
|
|
||||||
return getErrorURLParameters({
|
|
||||||
code: (error as AuthError)?.code,
|
|
||||||
error: message as string,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSuccess: true,
|
nextPath: nextUrl,
|
||||||
user,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user is from Keycloak provider
|
|
||||||
*/
|
|
||||||
private isKeycloakUser(user: any): boolean {
|
|
||||||
return user?.app_metadata?.provider === 'keycloak' ||
|
|
||||||
user?.app_metadata?.providers?.includes('keycloak');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setupMedusaUserForKeycloak(user: any): Promise<void> {
|
|
||||||
if (!user.email) {
|
|
||||||
console.warn('Keycloak user has no email, skipping Medusa setup');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if user already has medusa_account_id
|
|
||||||
const { data: accountData, error: fetchError } = await this.client
|
|
||||||
.schema('medreport')
|
|
||||||
.from('accounts')
|
|
||||||
.select('medusa_account_id, name, last_name')
|
|
||||||
.eq('primary_owner_user_id', user.id)
|
|
||||||
.eq('is_personal_account', true)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (fetchError && fetchError.code !== 'PGRST116') {
|
|
||||||
console.error('Error fetching account data for Keycloak user:', fetchError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user already has Medusa account, we're done
|
|
||||||
if (accountData?.medusa_account_id) {
|
|
||||||
console.log('Keycloak user already has Medusa account:', accountData.medusa_account_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { medusaLoginOrRegister } = await import('../../features/medusa-storefront/src/lib/data/customer');
|
|
||||||
|
|
||||||
const medusaAccountId = await medusaLoginOrRegister({
|
|
||||||
email: user.email,
|
|
||||||
supabaseUserId: user.id,
|
|
||||||
name: accountData?.name ?? '-',
|
|
||||||
lastName: accountData?.last_name ?? '-',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the account with the Medusa account ID
|
|
||||||
const { error: updateError } = await this.client
|
|
||||||
.schema('medreport')
|
|
||||||
.from('accounts')
|
|
||||||
.update({ medusa_account_id: medusaAccountId })
|
|
||||||
.eq('primary_owner_user_id', user.id)
|
|
||||||
.eq('is_personal_account', true);
|
|
||||||
|
|
||||||
if (updateError) {
|
|
||||||
console.error('Error updating account with Medusa ID:', updateError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Successfully set up Medusa account for Keycloak user:', medusaAccountId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error setting up Medusa account for Keycloak user:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private adjustUrlHostForLocalDevelopment(url: URL, host: string | null) {
|
private adjustUrlHostForLocalDevelopment(url: URL, host: string | null) {
|
||||||
if (this.isLocalhost(url.host) && !this.isLocalhost(host)) {
|
if (this.isLocalhost(url.host) && !this.isLocalhost(host)) {
|
||||||
url.host = host as string;
|
url.host = host as string;
|
||||||
@@ -260,19 +231,15 @@ class AuthCallbackService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorURLParameters {
|
function onError({
|
||||||
error: string;
|
|
||||||
code?: string;
|
|
||||||
searchParams: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getErrorURLParameters({
|
|
||||||
error,
|
error,
|
||||||
|
path,
|
||||||
code,
|
code,
|
||||||
}: {
|
}: {
|
||||||
error: string;
|
error: string;
|
||||||
|
path: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
}): ErrorURLParameters {
|
}) {
|
||||||
const errorMessage = getAuthErrorMessage({ error, code });
|
const errorMessage = getAuthErrorMessage({ error, code });
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
@@ -288,10 +255,10 @@ export function getErrorURLParameters({
|
|||||||
code: code ?? '',
|
code: code ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nextPath = `${path}?${searchParams.toString()}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: errorMessage,
|
nextPath,
|
||||||
code: code ?? '',
|
|
||||||
searchParams: searchParams.toString(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,5 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
|
|||||||
export function getSupabaseBrowserClient<GenericSchema = Database>() {
|
export function getSupabaseBrowserClient<GenericSchema = Database>() {
|
||||||
const keys = getSupabaseClientKeys();
|
const keys = getSupabaseClientKeys();
|
||||||
|
|
||||||
return createBrowserClient<GenericSchema>(keys.url, keys.anonKey, {
|
return createBrowserClient<GenericSchema>(keys.url, keys.anonKey);
|
||||||
auth: {
|
|
||||||
flowType: 'pkce',
|
|
||||||
autoRefreshToken: true,
|
|
||||||
persistSession: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ export function createMiddlewareClient<GenericSchema = Database>(
|
|||||||
const keys = getSupabaseClientKeys();
|
const keys = getSupabaseClientKeys();
|
||||||
|
|
||||||
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
|
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
|
||||||
auth: {
|
|
||||||
flowType: 'pkce',
|
|
||||||
autoRefreshToken: true,
|
|
||||||
persistSession: true,
|
|
||||||
},
|
|
||||||
cookies: {
|
cookies: {
|
||||||
getAll() {
|
getAll() {
|
||||||
return request.cookies.getAll();
|
return request.cookies.getAll();
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ export function getSupabaseServerClient<GenericSchema = Database>() {
|
|||||||
const keys = getSupabaseClientKeys();
|
const keys = getSupabaseClientKeys();
|
||||||
|
|
||||||
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
|
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
|
||||||
auth: {
|
|
||||||
flowType: 'pkce',
|
|
||||||
autoRefreshToken: true,
|
|
||||||
persistSession: true,
|
|
||||||
},
|
|
||||||
cookies: {
|
cookies: {
|
||||||
async getAll() {
|
async getAll() {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export function useSignInWithEmailPassword() {
|
|||||||
const medusaAccountId = await medusaLoginOrRegister({
|
const medusaAccountId = await medusaLoginOrRegister({
|
||||||
email: credentials.email,
|
email: credentials.email,
|
||||||
password: credentials.password,
|
password: credentials.password,
|
||||||
isDevPasswordLogin: true,
|
|
||||||
});
|
});
|
||||||
await client
|
await client
|
||||||
.schema('medreport').from('accounts')
|
.schema('medreport').from('accounts')
|
||||||
|
|||||||
@@ -9,13 +9,7 @@ export function useSignInWithProvider() {
|
|||||||
const mutationKey = ['auth', 'sign-in-with-provider'];
|
const mutationKey = ['auth', 'sign-in-with-provider'];
|
||||||
|
|
||||||
const mutationFn = async (credentials: SignInWithOAuthCredentials) => {
|
const mutationFn = async (credentials: SignInWithOAuthCredentials) => {
|
||||||
const response = await client.auth.signInWithOAuth({
|
const response = await client.auth.signInWithOAuth(credentials);
|
||||||
...credentials,
|
|
||||||
options: {
|
|
||||||
...credentials.options,
|
|
||||||
redirectTo: `${window.location.origin}/auth/callback`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw response.error.message;
|
throw response.error.message;
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useSupabase } from './use-supabase';
|
import { useSupabase } from './use-supabase';
|
||||||
|
import { signout } from '../../../features/medusa-storefront/src/lib/data/customer';
|
||||||
|
|
||||||
export function useSignOut() {
|
export function useSignOut() {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
try {
|
await signout(undefined, false);
|
||||||
try {
|
return client.auth.signOut();
|
||||||
const { medusaLogout } = await import('../../../features/medusa-storefront/src/lib/data/customer');
|
|
||||||
await medusaLogout();
|
|
||||||
} catch (medusaError) {
|
|
||||||
console.warn('Medusa logout failed or not available:', medusaError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error } = await client.auth.signOut();
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export function useSignUpWithEmailAndPassword() {
|
|||||||
const medusaAccountId = await medusaLoginOrRegister({
|
const medusaAccountId = await medusaLoginOrRegister({
|
||||||
email: credentials.email,
|
email: credentials.email,
|
||||||
password: credentials.password,
|
password: credentials.password,
|
||||||
isDevPasswordLogin: true,
|
|
||||||
});
|
});
|
||||||
await client
|
await client
|
||||||
.schema('medreport').from('accounts')
|
.schema('medreport').from('accounts')
|
||||||
|
|||||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -128,9 +128,6 @@ importers:
|
|||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: 9.0.2
|
specifier: 9.0.2
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
libphonenumber-js:
|
|
||||||
specifier: ^1.12.15
|
|
||||||
version: 1.12.15
|
|
||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
@@ -478,10 +475,10 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@keystatic/core':
|
'@keystatic/core':
|
||||||
specifier: 0.5.47
|
specifier: 0.5.47
|
||||||
version: 0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@keystatic/next':
|
'@keystatic/next':
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.0.4(@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 5.0.4(@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@markdoc/markdoc':
|
'@markdoc/markdoc':
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.4(@types/react@19.1.4)(react@19.1.0)
|
version: 0.5.4(@types/react@19.1.4)(react@19.1.0)
|
||||||
@@ -1272,7 +1269,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@sentry/nextjs':
|
'@sentry/nextjs':
|
||||||
specifier: ^9.19.0
|
specifier: ^9.19.0
|
||||||
version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)
|
version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)
|
||||||
import-in-the-middle:
|
import-in-the-middle:
|
||||||
specifier: 1.13.2
|
specifier: 1.13.2
|
||||||
version: 1.13.2
|
version: 1.13.2
|
||||||
@@ -8177,9 +8174,6 @@ packages:
|
|||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
libphonenumber-js@1.12.15:
|
|
||||||
resolution: {integrity: sha512-TMDCtIhWUDHh91wRC+wFuGlIzKdPzaTUHHVrIZ3vPUEoNaXFLrsIQ1ZpAeZeXApIF6rvDksMTvjrIQlLKaYxqQ==}
|
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -11461,7 +11455,7 @@ snapshots:
|
|||||||
|
|
||||||
'@juggle/resize-observer@3.4.0': {}
|
'@juggle/resize-observer@3.4.0': {}
|
||||||
|
|
||||||
'@keystar/ui@0.7.19(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@keystar/ui@0.7.19(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.6
|
'@babel/runtime': 7.27.6
|
||||||
'@emotion/css': 11.13.5
|
'@emotion/css': 11.13.5
|
||||||
@@ -11554,18 +11548,18 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.6
|
'@babel/runtime': 7.27.6
|
||||||
'@braintree/sanitize-url': 6.0.4
|
'@braintree/sanitize-url': 6.0.4
|
||||||
'@emotion/weak-memoize': 0.3.1
|
'@emotion/weak-memoize': 0.3.1
|
||||||
'@floating-ui/react': 0.24.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@floating-ui/react': 0.24.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@internationalized/string': 3.2.7
|
'@internationalized/string': 3.2.7
|
||||||
'@keystar/ui': 0.7.19(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@keystar/ui': 0.7.19(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@markdoc/markdoc': 0.4.0(@types/react@19.1.4)(react@19.1.0)
|
'@markdoc/markdoc': 0.4.0(@types/react@19.1.4)(react@19.1.0)
|
||||||
'@react-aria/focus': 3.20.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@react-aria/focus': 3.20.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -11636,13 +11630,13 @@ snapshots:
|
|||||||
- next
|
- next
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@keystatic/next@5.0.4(@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@keystatic/next@5.0.4(@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.6
|
'@babel/runtime': 7.27.6
|
||||||
'@keystatic/core': 0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@keystatic/core': 0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@types/react': 19.1.4
|
'@types/react': 19.1.4
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
server-only: 0.0.1
|
server-only: 0.0.1
|
||||||
@@ -17236,7 +17230,7 @@ snapshots:
|
|||||||
|
|
||||||
'@sentry/core@9.46.0': {}
|
'@sentry/core@9.46.0': {}
|
||||||
|
|
||||||
'@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)':
|
'@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/semantic-conventions': 1.34.0
|
'@opentelemetry/semantic-conventions': 1.34.0
|
||||||
@@ -17249,7 +17243,7 @@ snapshots:
|
|||||||
'@sentry/vercel-edge': 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))
|
'@sentry/vercel-edge': 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))
|
||||||
'@sentry/webpack-plugin': 3.5.0(webpack@5.101.3)
|
'@sentry/webpack-plugin': 3.5.0(webpack@5.101.3)
|
||||||
chalk: 3.0.0
|
chalk: 3.0.0
|
||||||
next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
rollup: 4.35.0
|
rollup: 4.35.0
|
||||||
stacktrace-parser: 0.1.11
|
stacktrace-parser: 0.1.11
|
||||||
@@ -20636,8 +20630,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isomorphic.js: 0.2.5
|
isomorphic.js: 0.2.5
|
||||||
|
|
||||||
libphonenumber-js@1.12.15: {}
|
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -21273,6 +21265,31 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
|
next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@next/env': 15.5.2
|
||||||
|
'@swc/helpers': 0.5.15
|
||||||
|
caniuse-lite: 1.0.30001723
|
||||||
|
postcss: 8.4.31
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
styled-jsx: 5.1.6(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@next/swc-darwin-arm64': 15.5.2
|
||||||
|
'@next/swc-darwin-x64': 15.5.2
|
||||||
|
'@next/swc-linux-arm64-gnu': 15.5.2
|
||||||
|
'@next/swc-linux-arm64-musl': 15.5.2
|
||||||
|
'@next/swc-linux-x64-gnu': 15.5.2
|
||||||
|
'@next/swc-linux-x64-musl': 15.5.2
|
||||||
|
'@next/swc-win32-arm64-msvc': 15.5.2
|
||||||
|
'@next/swc-win32-x64-msvc': 15.5.2
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
babel-plugin-react-compiler: 19.1.0-rc.2
|
||||||
|
sharp: 0.34.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@babel/core'
|
||||||
|
- babel-plugin-macros
|
||||||
|
|
||||||
no-case@3.0.4:
|
no-case@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
lower-case: 2.0.2
|
lower-case: 2.0.2
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Terms of Service",
|
"termsOfService": "Terms of Service",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
"orContinueWith": "Continue with",
|
"orContinueWith": "Or continue with",
|
||||||
"redirecting": "You're in! Please wait...",
|
"redirecting": "You're in! Please wait...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"Invalid login credentials": "The credentials entered are invalid",
|
"Invalid login credentials": "The credentials entered are invalid",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"emptyCartMessageDescription": "Add items to your cart to continue.",
|
"emptyCartMessageDescription": "Add items to your cart to continue.",
|
||||||
"subtotal": "Subtotal",
|
"subtotal": "Subtotal",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"promotionsTotal": "Promotions total",
|
||||||
"table": {
|
"table": {
|
||||||
"item": "Item",
|
"item": "Item",
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
@@ -24,10 +25,13 @@
|
|||||||
"timeoutAction": "Continue"
|
"timeoutAction": "Continue"
|
||||||
},
|
},
|
||||||
"discountCode": {
|
"discountCode": {
|
||||||
|
"title": "Gift card or promotion code",
|
||||||
"label": "Add Promotion Code(s)",
|
"label": "Add Promotion Code(s)",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"subtitle": "If you wish, you can add a promotion code",
|
"subtitle": "If you wish, you can add a promotion code",
|
||||||
"placeholder": "Enter promotion code"
|
"placeholder": "Enter promotion code",
|
||||||
|
"remove": "Remove promotion code",
|
||||||
|
"appliedCodes": "Promotion(s) applied:"
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"synlabAnalyses": {
|
"synlabAnalyses": {
|
||||||
|
|||||||
@@ -128,9 +128,6 @@
|
|||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"selectDate": "Select date"
|
"selectDate": "Select date"
|
||||||
},
|
},
|
||||||
"formFieldError": {
|
|
||||||
"invalidPhoneNumber": "Please enter a valid Estonian phone number (must include country code +372)"
|
|
||||||
},
|
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"balance": "Your MedReport account balance",
|
"balance": "Your MedReport account balance",
|
||||||
"expiredAt": "Valid until {{expiredAt}}"
|
"expiredAt": "Valid until {{expiredAt}}"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Kasutustingimused",
|
"termsOfService": "Kasutustingimused",
|
||||||
"privacyPolicy": "Privaatsuspoliitika",
|
"privacyPolicy": "Privaatsuspoliitika",
|
||||||
"orContinueWith": "Jätka",
|
"orContinueWith": "Või jätka koos",
|
||||||
"redirecting": "Oled sees! Palun oota...",
|
"redirecting": "Oled sees! Palun oota...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"Invalid login credentials": "Sisestatud andmed on valed",
|
"Invalid login credentials": "Sisestatud andmed on valed",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"emptyCartMessage": "Sinu ostukorv on tühi",
|
"emptyCartMessage": "Sinu ostukorv on tühi",
|
||||||
"emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.",
|
"emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.",
|
||||||
"subtotal": "Vahesumma",
|
"subtotal": "Vahesumma",
|
||||||
|
"promotionsTotal": "Soodustuse summa",
|
||||||
"total": "Summa",
|
"total": "Summa",
|
||||||
"table": {
|
"table": {
|
||||||
"item": "Toode",
|
"item": "Toode",
|
||||||
@@ -28,7 +29,13 @@
|
|||||||
"label": "Lisa promo kood",
|
"label": "Lisa promo kood",
|
||||||
"apply": "Rakenda",
|
"apply": "Rakenda",
|
||||||
"subtitle": "Kui soovid, võid lisada promo koodi",
|
"subtitle": "Kui soovid, võid lisada promo koodi",
|
||||||
"placeholder": "Sisesta promo kood"
|
"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"
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"synlabAnalyses": {
|
"synlabAnalyses": {
|
||||||
|
|||||||
@@ -128,9 +128,6 @@
|
|||||||
"amount": "Summa",
|
"amount": "Summa",
|
||||||
"selectDate": "Vali kuupäev"
|
"selectDate": "Vali kuupäev"
|
||||||
},
|
},
|
||||||
"formFieldError": {
|
|
||||||
"invalidPhoneNumber": "Palun sisesta Eesti telefoninumber (peab sisaldama riigikoodi +372)"
|
|
||||||
},
|
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"balance": "Sinu MedReporti konto saldo",
|
"balance": "Sinu MedReporti konto saldo",
|
||||||
"expiredAt": "Kehtiv kuni {{expiredAt}}"
|
"expiredAt": "Kehtiv kuni {{expiredAt}}"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
"acceptTermsAndConditions": "Я принимаю <TermsOfServiceLink /> и <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "Я принимаю <TermsOfServiceLink /> и <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Условия использования",
|
"termsOfService": "Условия использования",
|
||||||
"privacyPolicy": "Политика конфиденциальности",
|
"privacyPolicy": "Политика конфиденциальности",
|
||||||
"orContinueWith": "Продолжить через",
|
"orContinueWith": "Или продолжить через",
|
||||||
"redirecting": "Вы вошли! Пожалуйста, подождите...",
|
"redirecting": "Вы вошли! Пожалуйста, подождите...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"Invalid login credentials": "Введенные данные недействительны",
|
"Invalid login credentials": "Введенные данные недействительны",
|
||||||
|
|||||||
8
run-test-sync-local.sh
Executable file → Normal file
8
run-test-sync-local.sh
Executable file → Normal file
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
MEDUSA_ORDER_ID="order_01K2JFCZ609YF5G84ZKDCBWPM6"
|
MEDUSA_ORDER_ID="order_01K1TQQHZGPXKDHAH81TDSNGXR"
|
||||||
|
|
||||||
# HOSTNAME="https://test.medreport.ee"
|
# HOSTNAME="https://test.medreport.ee"
|
||||||
# JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84"
|
# JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84"
|
||||||
@@ -33,7 +33,7 @@ function sync_analysis_groups_store() {
|
|||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
# 1. Sync analysis groups from Medipost to B2B
|
# 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)
|
# 2. Optional - sync all Medipost analysis groups from B2B to Medusa (or add manually)
|
||||||
#sync_analysis_groups_store
|
#sync_analysis_groups_store
|
||||||
@@ -41,7 +41,7 @@ function sync_analysis_groups_store() {
|
|||||||
# 3. Set up products configurations in Medusa so B2B "Telli analüüs" page shows the product and you can do payment flow
|
# 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
|
# 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
|
# 5. Run `sync_analysis_results` to sync the all new Medipost results to B2B
|
||||||
sync_analysis_results
|
# sync_analysis_results
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
docker build -t medreport-b2b:latest .
|
|
||||||
|
|
||||||
# Get size of built image and display it
|
|
||||||
@@ -105,9 +105,9 @@ file_size_limit = "50MiB"
|
|||||||
enabled = true
|
enabled = true
|
||||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||||
# in emails.
|
# in emails.
|
||||||
site_url = "http://localhost:3000"
|
site_url = "http://127.0.0.1:3000"
|
||||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
# 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://127.0.0.1:3000","http://localhost:3000","http://localhost:3000/auth/callback", "http://localhost:3000/update-password"]
|
additional_redirect_urls = ["https://127.0.0.1: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).
|
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||||
jwt_expiry = 3600
|
jwt_expiry = 3600
|
||||||
# If disabled, the refresh token will never expire.
|
# If disabled, the refresh token will never expire.
|
||||||
@@ -269,14 +269,6 @@ url = ""
|
|||||||
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||||
skip_nonce_check = false
|
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.
|
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||||
[auth.third_party.firebase]
|
[auth.third_party.firebase]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
-- Update the user creation trigger to properly handle Keycloak user metadata
|
|
||||||
CREATE OR REPLACE FUNCTION kit.setup_new_user()
|
|
||||||
RETURNS trigger
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
SECURITY DEFINER
|
|
||||||
SET search_path TO ''
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
user_name text;
|
|
||||||
picture_url text;
|
|
||||||
personal_code text;
|
|
||||||
full_name text;
|
|
||||||
given_name text;
|
|
||||||
family_name text;
|
|
||||||
preferred_username text;
|
|
||||||
BEGIN
|
|
||||||
-- Extract data from Keycloak user metadata
|
|
||||||
-- Check raw_user_meta_data first (this is where Keycloak data is stored)
|
|
||||||
IF new.raw_user_meta_data IS NOT NULL THEN
|
|
||||||
-- Try full_name first, then name field
|
|
||||||
full_name := new.raw_user_meta_data ->> 'full_name';
|
|
||||||
IF full_name IS NULL THEN
|
|
||||||
full_name := new.raw_user_meta_data ->> 'name';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Extract individual name components
|
|
||||||
given_name := new.raw_user_meta_data -> 'custom_claims' ->> 'given_name';
|
|
||||||
family_name := new.raw_user_meta_data -> 'custom_claims' ->> 'family_name';
|
|
||||||
preferred_username := new.raw_user_meta_data -> 'custom_claims' ->> 'preferred_username';
|
|
||||||
|
|
||||||
-- Use given_name (first name) for the name field
|
|
||||||
IF given_name IS NOT NULL THEN
|
|
||||||
user_name := given_name;
|
|
||||||
ELSIF full_name IS NOT NULL THEN
|
|
||||||
user_name := full_name;
|
|
||||||
ELSIF preferred_username IS NOT NULL THEN
|
|
||||||
user_name := preferred_username;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Extract personal code from preferred_username (Keycloak provides Estonian personal codes here)
|
|
||||||
IF preferred_username IS NOT NULL THEN
|
|
||||||
personal_code := preferred_username;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Also try personalCode field as fallback
|
|
||||||
IF personal_code IS NULL THEN
|
|
||||||
personal_code := new.raw_user_meta_data ->> 'personalCode';
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Fall back to email if no name found
|
|
||||||
IF user_name IS NULL AND new.email IS NOT NULL THEN
|
|
||||||
user_name := split_part(new.email, '@', 1);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Default empty string if still no name
|
|
||||||
IF user_name IS NULL THEN
|
|
||||||
user_name := '';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Extract picture URL
|
|
||||||
IF new.raw_user_meta_data ->> 'avatar_url' IS NOT NULL THEN
|
|
||||||
picture_url := new.raw_user_meta_data ->> 'avatar_url';
|
|
||||||
ELSE
|
|
||||||
picture_url := null;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Insert into medreport.accounts
|
|
||||||
INSERT INTO medreport.accounts (
|
|
||||||
id,
|
|
||||||
primary_owner_user_id,
|
|
||||||
name,
|
|
||||||
last_name,
|
|
||||||
is_personal_account,
|
|
||||||
picture_url,
|
|
||||||
email,
|
|
||||||
personal_code,
|
|
||||||
application_role
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
new.id,
|
|
||||||
new.id,
|
|
||||||
user_name,
|
|
||||||
family_name,
|
|
||||||
true,
|
|
||||||
picture_url,
|
|
||||||
NULL, -- Keycloak email !== customer personal email, they will set this later
|
|
||||||
personal_code,
|
|
||||||
'user' -- Default role for new users
|
|
||||||
);
|
|
||||||
|
|
||||||
RETURN new;
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# 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.
|
|
||||||
Reference in New Issue
Block a user