Compare commits
10 Commits
5ef7f58f5d
...
keycloak2
| Author | SHA1 | Date | |
|---|---|---|---|
| ab92c3b4d0 | |||
| e324872c3c | |||
| a44f9c9207 | |||
| 0cf04b4f55 | |||
| 84216c3ced | |||
| 0037241558 | |||
| c7f89723e3 | |||
| 4a06059a25 | |||
| a0abb44257 | |||
| 283b502963 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.git
|
||||
Dockerfile
|
||||
3
.env
3
.env
@@ -68,6 +68,3 @@ NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
||||
|
||||
# Configure Medusa password secret for Keycloak users
|
||||
MEDUSA_PASSWORD_SECRET=ODEwMGNiMmUtOGMxYS0xMWYwLWJlZDYtYTM3YzYyMWY0NGEzCg==
|
||||
|
||||
# False by default
|
||||
MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=false
|
||||
|
||||
@@ -10,10 +10,6 @@ NEXT_PUBLIC_AUTH_PASSWORD=true
|
||||
## THIS IS FOR DEVELOPMENT ONLY - DO NOT USE IN PRODUCTION
|
||||
SUPABASE_DB_WEBHOOK_SECRET=WEBHOOKSECRET
|
||||
|
||||
# MEDUSA MONTONIO URLS FOR LOCALHOST
|
||||
# Montonio doesn't allow localhost as notification/callback URL
|
||||
DEV_MONTONIO_CALLBACK_URL=http://webhook.site:3000
|
||||
|
||||
# EMAILS
|
||||
|
||||
# CONTACT FORM
|
||||
@@ -37,25 +33,58 @@ MEDIPOST_USER=trvurgtst
|
||||
MEDIPOST_PASSWORD=SRB48HZMV
|
||||
MEDIPOST_RECIPIENT=trvurgtst
|
||||
MEDIPOST_MESSAGE_SENDER=trvurgtst
|
||||
MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=true
|
||||
MEDIPOST_ENABLE_DELETE_RESPONSE_PRIVATE_MESSAGE_ON_READ=false
|
||||
|
||||
#MEDIPOST_URL=https://medipost2.medisoft.ee:8443/Medipost/MedipostServlet
|
||||
#MEDIPOST_USER=medreport
|
||||
#MEDIPOST_PASSWORD=
|
||||
#MEDIPOST_PASSWORD=85MXFFDB7
|
||||
#MEDIPOST_RECIPIENT=HTI
|
||||
#MEDIPOST_MESSAGE_SENDER=medreport
|
||||
#MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=false
|
||||
|
||||
# MEDUSA
|
||||
COMPANY_BENEFITS_PAYMENT_SECRET_KEY=NzcwMzE2NmEtOThiMS0xMWYwLWI4NjYtMDMwZDQzMjFhMjExCg==
|
||||
MEDUSA_BACKEND_URL=http://localhost:9000
|
||||
MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000
|
||||
MEDUSA_SECRET_API_KEY=sk_b332d525212ab4078ef73fb2b8232c3beebccc4a460e2c7abf6e187a458d60cf
|
||||
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_e23a820689a07d55aa0a0ad187268559f5d6288ecb0768ff4520516285bdef84
|
||||
|
||||
#MEDUSA_BACKEND_URL=https://backoffice-test.medreport.ee
|
||||
#MEDUSA_BACKEND_PUBLIC_URL=https://backoffice-test.medreport.ee
|
||||
#MEDUSA_SECRET_API_KEY=sk_fdb1808fbabf62979cc46316aa997378ffbb87882883e8f5c3ee47cee39dcac5
|
||||
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_827a2ab863021cb67993f1d81078f81bfce4b4e0da642d8c0f5398ded9d8fd32
|
||||
|
||||
#MEDUSA_BACKEND_URL=https://backoffice.medreport.ee
|
||||
#MEDUSA_BACKEND_PUBLIC_URL=https://backoffice.medreport.ee
|
||||
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_068d930c33fea53608a410d84a51935f6ce2ccec5bef8e0ecf75eaee602ac486
|
||||
#MEDUSA_SECRET_API_KEY=sk_fdb1808fbabf62979cc46316aa997378ffbb87882883e8f5c3ee47cee39dcac5
|
||||
|
||||
#MEDUSA_BACKEND_URL=http://5.181.51.38:9000
|
||||
#MEDUSA_BACKEND_PUBLIC_URL=http://5.181.51.38:9000
|
||||
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_0ec86252438b38ce18d5601f7877e4395d7e0a6afa8687dfea8d37af33015633
|
||||
|
||||
# MONTONIO
|
||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
|
||||
MONTONIO_API_URL=https://sandbox-stargate.montonio.com
|
||||
|
||||
#NEXT_PUBLIC_MONTONIO_ACCESS_KEY=13e3686a-e7ad-41f6-998b-3f7d7de17654
|
||||
#MONTONIO_SECRET_KEY=wTd4BZ01h80KZLMPL4mjt0RCFxKaYRSu9mMB1PQZCxnw
|
||||
#MONTONIO_API_URL=https://stargate.montonio.com
|
||||
|
||||
# JOBS
|
||||
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
|
||||
|
||||
# SUPABASE
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1Njk4NDYyOCwiZXhwIjoyMDcyNTYwNjI4fQ.1UZR7AqSD9bOy1gtZRGhOCNoESsw2W-DoFDDsNNMwoE
|
||||
|
||||
#NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
||||
#NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
|
||||
#SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0NjUyODEyMywiZXhwIjoyMDYyMTA0MTIzfQ.KVcnkZ21Pd0XkJho23dZqFHawVTLQqfvF7l2RxsELLk
|
||||
|
||||
#NEXT_PUBLIC_SUPABASE_URL=http://5.181.51.38:54321
|
||||
#NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
||||
#SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
||||
|
||||
### TEST.MEDREPORT.ee ###
|
||||
|
||||
DB_PASSWORD=T#u-$M7%RjbA@L@
|
||||
|
||||
@@ -10,7 +10,6 @@ MEDIPOST_PASSWORD=your-medipost-password
|
||||
MEDIPOST_RECIPIENT=your-medipost-recipient
|
||||
|
||||
CONNECTED_ONLINE_URL=your-connected-online-url
|
||||
CONNECTED_ONLINE_CONFIRMED_URL=your-connected-confirmed-url
|
||||
|
||||
EMAIL_SENDER=
|
||||
EMAIL_USER= # refer to your email provider's documentation
|
||||
@@ -20,7 +19,6 @@ EMAIL_PORT= # or 465 for SSL
|
||||
EMAIL_TLS= # or false for SSL (see provider documentation)
|
||||
|
||||
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
||||
MEDUSA_SECRET_API_KEY=
|
||||
|
||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
||||
|
||||
# SUPABASE
|
||||
# NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
||||
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
|
||||
|
||||
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||
|
||||
# # MONTONIO
|
||||
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
# MONTONIO
|
||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
|
||||
10
.env.staging
10
.env.staging
@@ -6,10 +6,10 @@
|
||||
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
||||
|
||||
# SUPABASE
|
||||
# NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
|
||||
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://kaldvociniytdbbcxvqk.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImthbGR2b2Npbml5dGRiYmN4dnFrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTYzNjQ5OTYsImV4cCI6MjA3MTk0MDk5Nn0.eixihH2KGkJZolY9FiQDicJOo2kxvXrSe6gGUCrkLo0
|
||||
|
||||
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||
|
||||
# # MONTONIO
|
||||
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
# MONTONIO
|
||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
|
||||
1
.npmrc
1
.npmrc
@@ -3,7 +3,6 @@ dedupe-peer-dependents=true
|
||||
use-lockfile-v6=true
|
||||
resolution-mode=highest
|
||||
package-manager-strict=true
|
||||
ignore-workspace-root-check=true
|
||||
public-hoist-pattern[]=*i18next*
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=*prettier*
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
database.types.ts
|
||||
playwright-report
|
||||
*.hbs
|
||||
.history
|
||||
node_modules
|
||||
*.hbs
|
||||
16
Dockerfile
16
Dockerfile
@@ -22,12 +22,12 @@ COPY . .
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN set -a \
|
||||
&& . .env \
|
||||
&& . .env.production \
|
||||
&& . .env.staging \
|
||||
&& set +a \
|
||||
&& node check-env.js \
|
||||
&& pnpm build
|
||||
&& . .env \
|
||||
&& . .env.production \
|
||||
&& . .env.staging \
|
||||
&& set +a \
|
||||
&& node check-env.js \
|
||||
&& pnpm build
|
||||
|
||||
|
||||
# --- Stage 2: Runtime ---
|
||||
@@ -41,13 +41,13 @@ COPY --from=builder /app ./
|
||||
RUN cp ".env.${APP_ENV}" .env.local
|
||||
|
||||
RUN npm install -g pnpm@9 \
|
||||
&& pnpm install --prod --frozen-lockfile
|
||||
&& pnpm install --prod --frozen-lockfile
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# 🔍 Optional: Log key envs for debug
|
||||
RUN echo "📄 .env contents:" && cat .env.local \
|
||||
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
|
||||
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
|
||||
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
16
README.md
16
README.md
@@ -47,18 +47,6 @@ if you get missing dependency error do `pnpm i --force`
|
||||
pnpm add <pacakge-name> -w
|
||||
```
|
||||
|
||||
## Eslint and prettier
|
||||
|
||||
To clean code run
|
||||
|
||||
```bash
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run prettier:fix
|
||||
```
|
||||
|
||||
## Supabase
|
||||
|
||||
Start supabase in docker
|
||||
@@ -98,13 +86,13 @@ To access admin pages follow these steps:
|
||||
- Register new user
|
||||
- Go to Profile and add Multi-Factor Authentication
|
||||
- Authenticate with mfa (at current time profile page prompts it again)
|
||||
- update your `account.application_role` to `super_admin`.
|
||||
- update your role. look at `supabase/sql/super-admin.sql`
|
||||
- Sign out and Sign in
|
||||
|
||||
## Company User
|
||||
|
||||
- With admin account go to `http://localhost:3000/admin/accounts`
|
||||
- For Create Company Account to work you need to have rows in `medreport.roles` table.
|
||||
- For Create Company Account to work you need to have rows in `medreport.roles` table. For that you can sql in `supabase/sql/super-admin.sql`
|
||||
|
||||
## Start email server
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Mock for isikukood library to avoid ES module issues in tests
|
||||
|
||||
export enum Gender {
|
||||
MALE = 'male',
|
||||
FEMALE = 'female',
|
||||
}
|
||||
|
||||
export default class Isikukood {
|
||||
private code: string;
|
||||
|
||||
constructor(code: string) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
static validate(code: string): boolean {
|
||||
return true; // Mock always returns true for tests
|
||||
}
|
||||
|
||||
static generate(options?: { gender?: Gender; century?: number }): string {
|
||||
return '39001010002'; // Mock Estonian ID code
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getGender(): Gender {
|
||||
return Gender.MALE;
|
||||
}
|
||||
|
||||
getBirthDate(): Date {
|
||||
return new Date('1990-01-01');
|
||||
}
|
||||
|
||||
getAge(): number {
|
||||
return 30;
|
||||
}
|
||||
|
||||
getCentury(): number {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
export { Isikukood };
|
||||
@@ -1,3 +0,0 @@
|
||||
// Mock for server-only to avoid Next.js server component issues in tests
|
||||
// This module does nothing in tests - it's just a marker for Next.js
|
||||
export {};
|
||||
@@ -2,6 +2,7 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||
import { useAuthConfig } from '@kit/shared/hooks';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { authConfig, featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||
|
||||
const ModeToggle = dynamic(() =>
|
||||
import('@kit/ui/mode-toggle').then((mod) => ({
|
||||
default: mod.ModeToggle,
|
||||
@@ -57,8 +57,6 @@ export function SiteHeaderAccountSection({
|
||||
}
|
||||
|
||||
function AuthButtons() {
|
||||
const { config } = useAuthConfig();
|
||||
|
||||
return (
|
||||
<div className={'animate-in fade-in flex gap-x-2.5 duration-500'}>
|
||||
<div className={'hidden md:flex'}>
|
||||
@@ -67,25 +65,21 @@ function AuthButtons() {
|
||||
</If>
|
||||
</div>
|
||||
|
||||
{config && (
|
||||
<div className={'flex gap-x-2.5'}>
|
||||
{(config.providers.password || config.providers.oAuth.length > 0) && (
|
||||
<Button className={'block'} asChild variant={'ghost'}>
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Trans i18nKey={'auth:signIn'} />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<div className={'flex gap-x-2.5'}>
|
||||
<Button className={'block'} asChild variant={'ghost'}>
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Trans i18nKey={'auth:signIn'} />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{config.providers.password && (
|
||||
<Button asChild className="text-xs md:text-sm" variant={'default'}>
|
||||
<Link href={pathsConfig.auth.signUp}>
|
||||
<Trans i18nKey={'auth:signUp'} />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{authConfig.providers.password && (
|
||||
<Button asChild className="text-xs md:text-sm" variant={'default'}>
|
||||
<Link href={pathsConfig.auth.signUp}>
|
||||
<Trans i18nKey={'auth:signUp'} />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { Header } from '@kit/ui/marketing';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
|
||||
import { SiteHeaderAccountSection } from './site-header-account-section';
|
||||
|
||||
export function SiteHeader({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createCmsClient } from '@kit/cms';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
import { Post } from '../../blog/_components/post';
|
||||
|
||||
interface BlogPageProps {
|
||||
|
||||
@@ -11,16 +11,14 @@ const contactEmail = z
|
||||
.string({
|
||||
error:
|
||||
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
|
||||
})
|
||||
.describe(`The email where you want to receive the contact form submissions.`)
|
||||
}).describe(`The email where you want to receive the contact form submissions.`)
|
||||
.parse(process.env.CONTACT_EMAIL);
|
||||
|
||||
const emailFrom = z
|
||||
.string({
|
||||
error:
|
||||
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
||||
})
|
||||
.describe(`The email sending address.`)
|
||||
}).describe(`The email sending address.`)
|
||||
.parse(process.env.EMAIL_SENDER);
|
||||
|
||||
export const sendContactEmail = enhanceAction(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import { use } from 'react';
|
||||
|
||||
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||
import { loadCurrentUserAccounts } from '~/home/(user)/_lib/server/load-accounts';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { loadCurrentUserAccounts } from '~/home/(user)/_lib/server/load-accounts';
|
||||
|
||||
function SiteLayout(props: React.PropsWithChildren) {
|
||||
const accounts = use(loadCurrentUserAccounts());
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
|
||||
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
|
||||
import { CtaButton, Hero } from '@kit/ui/marketing';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ import { Label } from '@kit/ui/label';
|
||||
import { Spinner } from '@kit/ui/spinner';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { sendCompanyOfferEmail } from '../_lib/server/company-offer-actions';
|
||||
|
||||
const CompanyOfferForm = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
@@ -36,16 +34,6 @@ const CompanyOfferForm = () => {
|
||||
|
||||
const onSubmit = async (data: CompanySubmitData) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await sendCompanyOfferEmail(data, language);
|
||||
router.push('/company-offer/success');
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
if (err instanceof Error) {
|
||||
console.warn('Could not send company offer email: ' + err.message);
|
||||
}
|
||||
console.warn('Could not send company offer email: ', err);
|
||||
}
|
||||
const formData = new FormData();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== undefined) formData.append(key, value);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { renderCompanyOfferEmail } from '@/packages/email-templates/src';
|
||||
|
||||
import { sendEmailFromTemplate } from '~/lib/services/mailer.service';
|
||||
import { CompanySubmitData } from '~/lib/types/company';
|
||||
|
||||
export const sendCompanyOfferEmail = async (
|
||||
data: CompanySubmitData,
|
||||
language: string,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== undefined) formData.append(key, value);
|
||||
});
|
||||
|
||||
sendEmailFromTemplate(
|
||||
renderCompanyOfferEmail,
|
||||
{
|
||||
companyData: data,
|
||||
language,
|
||||
},
|
||||
process.env.CONTACT_EMAIL!,
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import CompanyOfferForm from './_components/company-offer-form';
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
function SiteLayout(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
||||
<div className={'flex min-h-[100vh] flex-col justify-center items-center'}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { SIDEBAR_WIDTH_PROPERTY } from '@/packages/ui/src/shadcn/constants';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||
|
||||
import type { UserWorkspace } from '../../home/(user)/_lib/server/load-user-workspace';
|
||||
|
||||
export function AdminMenuNavigation(props: { workspace: UserWorkspace }) {
|
||||
const { accounts } = props.workspace;
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-1 items-center justify-between gap-3'}>
|
||||
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
|
||||
<AppLogo href={'/admin'} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<div>
|
||||
<ProfileAccountDropdownContainer accounts={accounts} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,73 @@
|
||||
import { adminNavigationConfig } from '@kit/shared/config';
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
|
||||
import { LayoutDashboard, Users } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarNavigation,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
useSidebar,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
export function AdminSidebar() {
|
||||
const collapsible = adminNavigationConfig.sidebarCollapsedStyle;
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||
|
||||
export function AdminSidebar({
|
||||
accounts,
|
||||
}: {
|
||||
accounts: UserWorkspace['accounts'];
|
||||
}) {
|
||||
const path = usePathname();
|
||||
const { open } = useSidebar();
|
||||
return (
|
||||
<Sidebar collapsible={collapsible}>
|
||||
<SidebarHeader className="h-24 justify-center">
|
||||
<div className="mt-24 flex items-center">
|
||||
<h5>Superadmin</h5>
|
||||
</div>
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className={'m-2'}>
|
||||
<AppLogo href={'/admin'} className="max-w-full" compact={!open} />
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarNavigation config={adminNavigationConfig} />
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Super Admin</SidebarGroupLabel>
|
||||
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuButton isActive={path === '/admin'} asChild>
|
||||
<Link className={'flex gap-2.5'} href={'/admin'}>
|
||||
<LayoutDashboard className={'h-4'} />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<SidebarMenuButton
|
||||
isActive={path.includes('/admin/accounts')}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className={'flex size-full gap-2.5'}
|
||||
href={'/admin/accounts'}
|
||||
>
|
||||
<Users className={'h-4'} />
|
||||
<span>Accounts</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<ProfileAccountDropdownContainer accounts={accounts} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { cache } from 'react';
|
||||
|
||||
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { getAccount } from '~/lib/services/account.service';
|
||||
|
||||
interface Params {
|
||||
@@ -28,6 +26,6 @@ async function AccountPage(props: Params) {
|
||||
return <AdminAccountPage account={account} />;
|
||||
}
|
||||
|
||||
export default withI18n(AdminGuard(AccountPage));
|
||||
export default AdminGuard(AccountPage);
|
||||
|
||||
const loadAccount = cache(getAccount);
|
||||
|
||||
@@ -9,8 +9,6 @@ import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
interface SearchParams {
|
||||
page?: string;
|
||||
account_type?: 'all' | 'team' | 'personal';
|
||||
@@ -32,7 +30,7 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader description={<AppBreadcrumbs />} title="Accounts">
|
||||
<PageHeader description={<AppBreadcrumbs />}>
|
||||
<div className="flex justify-end gap-2">
|
||||
<AdminCreateUserDialog>
|
||||
<Button data-test="admin-create-user-button">
|
||||
@@ -86,4 +84,4 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(AdminGuard(AccountsPage));
|
||||
export default AdminGuard(AccountsPage);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { cookies } from 'next/headers';
|
||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { AdminMenuNavigation } from '~/admin/_components/admin-menu-navigation';
|
||||
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
|
||||
|
||||
@@ -22,24 +21,19 @@ export default function AdminLayout(props: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
|
||||
return (
|
||||
<Page style={'header'}>
|
||||
<PageNavigation>
|
||||
<AdminMenuNavigation workspace={workspace} />
|
||||
</PageNavigation>
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<AdminSidebar accounts={workspace.accounts} />
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation>
|
||||
<AdminMobileNavigation />
|
||||
</PageMobileNavigation>
|
||||
<PageMobileNavigation>
|
||||
<AdminMobileNavigation />
|
||||
</PageMobileNavigation>
|
||||
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<AdminSidebar />
|
||||
</PageNavigation>
|
||||
{props.children}
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
</Page>
|
||||
{props.children}
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ import { AdminDashboard } from '@kit/admin/components/admin-dashboard';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
function AdminPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={`Super Admin`} />
|
||||
<PageHeader description={`Super Admin`} />
|
||||
|
||||
<PageBody>
|
||||
<AdminDashboard />
|
||||
@@ -16,4 +14,4 @@ function AdminPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(AdminGuard(AdminPage));
|
||||
export default AdminGuard(AdminPage);
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { enhanceRouteHandler } from '@/packages/next/src/routes';
|
||||
import { createAuthCallbackService } from '@/packages/supabase/src/auth-callback.service';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
export const POST = enhanceRouteHandler(
|
||||
async () => {
|
||||
try {
|
||||
const supabaseClient = getSupabaseServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseClient.auth.getUser();
|
||||
const service = createAuthCallbackService(supabaseClient);
|
||||
|
||||
if (user && service.isKeycloakUser(user)) {
|
||||
await service.setupMedusaUserForKeycloak(user);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('Error on verifying:', { err });
|
||||
return new Response(null, { status: 500 });
|
||||
}
|
||||
},
|
||||
{
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
@@ -1,10 +1,11 @@
|
||||
import { getPlanTypesMap } from '@kit/billing';
|
||||
import { getBillingEventHandlerService } from '@kit/billing-gateway';
|
||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
import { billingConfig } from '@kit/shared/config';
|
||||
|
||||
/**
|
||||
* @description Handle the webhooks from Stripe related to checkouts
|
||||
*/
|
||||
|
||||
21
app/api/email-template-test/route.ts
Normal file
21
app/api/email-template-test/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { sendEmail } from "~/lib/services/mailer.service";
|
||||
|
||||
export const GET = async () => {
|
||||
const { renderInviteEmail } = await import('@kit/email-templates');
|
||||
|
||||
const html = await renderInviteEmail({
|
||||
language: 'en',
|
||||
teamName: 'Test Team',
|
||||
invitedUserEmail: 'test@example.com',
|
||||
productName: 'Test Product',
|
||||
teamLogo: 'https://placehold.co/100x100',
|
||||
inviter: 'John Doe',
|
||||
link: 'https://www.google.com',
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
html,
|
||||
length: html.html.length,
|
||||
});
|
||||
};
|
||||
@@ -2,11 +2,7 @@ import { config } from 'dotenv';
|
||||
|
||||
export default function loadEnv() {
|
||||
config({ path: `.env` });
|
||||
if (
|
||||
['local', 'test', 'development', 'production'].includes(
|
||||
process.env.NODE_ENV!,
|
||||
)
|
||||
) {
|
||||
if (['local', 'test', 'development', 'production'].includes(process.env.NODE_ENV!)) {
|
||||
config({ path: `.env.${process.env.NODE_ENV}` });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,10 @@ import { sendEmailFromTemplate } from '~/lib/services/mailer.service';
|
||||
export default async function sendOpenJobsEmails() {
|
||||
const analysisResponseIds = await getOpenJobAnalysisResponseIds();
|
||||
|
||||
if (analysisResponseIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doctorAccounts = await getDoctorAccounts();
|
||||
const doctorEmails = doctorAccounts
|
||||
const doctorEmails: string[] = doctorAccounts
|
||||
.map(({ email }) => email)
|
||||
.filter((email) => !!email);
|
||||
|
||||
if (doctorEmails !== null) {
|
||||
return [];
|
||||
}
|
||||
.filter((email): email is string => !!email);
|
||||
|
||||
await sendEmailFromTemplate(
|
||||
renderNewJobsAvailableEmail,
|
||||
@@ -28,6 +20,4 @@ export default async function sendOpenJobsEmails() {
|
||||
},
|
||||
doctorEmails,
|
||||
);
|
||||
|
||||
return doctorAccounts.filter((email) => !!email).map(({ id }) => id);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { listProductTypes } from '@lib/data/products';
|
||||
import Medusa from '@medusajs/js-sdk';
|
||||
import type { AdminProductCategory } from '@medusajs/types';
|
||||
|
||||
import {
|
||||
createMedusaSyncFailEntry,
|
||||
createMedusaSyncSuccessEntry,
|
||||
} from '~/lib/services/analyses.service';
|
||||
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
|
||||
import { getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
||||
import Medusa from "@medusajs/js-sdk"
|
||||
import type { AdminProductCategory } from "@medusajs/types";
|
||||
import { listProductTypes } from "@lib/data/products";
|
||||
import { getAnalysisElements } from "~/lib/services/analysis-element.service";
|
||||
import { getAnalysisGroups } from "~/lib/services/analysis-group.service";
|
||||
import { createMedusaSyncFailEntry, createMedusaSyncSuccessEntry } from "~/lib/services/analyses.service";
|
||||
|
||||
const SYNLAB_SERVICES_CATEGORY_HANDLE = 'synlab-services';
|
||||
const SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE = 'synlab-analysis';
|
||||
@@ -16,8 +12,7 @@ const BASE_ANALYSIS_PRODUCT_HANDLE = 'analysis-base';
|
||||
|
||||
const getAdminSdk = () => {
|
||||
const medusaBackendUrl = process.env.MEDUSA_BACKEND_PUBLIC_URL!;
|
||||
const medusaPublishableApiKey =
|
||||
process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!;
|
||||
const medusaPublishableApiKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!;
|
||||
const key = process.env.MEDUSA_SECRET_API_KEY!;
|
||||
|
||||
if (!medusaBackendUrl || !medusaPublishableApiKey) {
|
||||
@@ -28,14 +23,15 @@ const getAdminSdk = () => {
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
apiKey: key,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function createProductCategories({ medusa }: { medusa: Medusa }) {
|
||||
const { product_categories: existingProductCategories } =
|
||||
await medusa.admin.productCategory.list();
|
||||
const parentCategory = existingProductCategories.find(
|
||||
({ handle }) => handle === SYNLAB_SERVICES_CATEGORY_HANDLE,
|
||||
);
|
||||
async function createProductCategories({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { product_categories: existingProductCategories } = await medusa.admin.productCategory.list();
|
||||
const parentCategory = existingProductCategories.find(({ handle }) => handle === SYNLAB_SERVICES_CATEGORY_HANDLE);
|
||||
|
||||
if (!parentCategory) {
|
||||
throw new Error('Parent category not found');
|
||||
@@ -50,12 +46,8 @@ async function createProductCategories({ medusa }: { medusa: Medusa }) {
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
console.info(`Processing analysis group '${analysisGroup.name}'`);
|
||||
|
||||
const isExisting = existingProductCategories.find(
|
||||
({ name }) => name === analysisGroup.name,
|
||||
);
|
||||
const isNewlyCreated = createdCategories.find(
|
||||
({ name }) => name === analysisGroup.name,
|
||||
);
|
||||
const isExisting = existingProductCategories.find(({ name }) => name === analysisGroup.name);
|
||||
const isNewlyCreated = createdCategories.find(({ name }) => name === analysisGroup.name);
|
||||
if (isExisting || isNewlyCreated) {
|
||||
console.info(`Analysis group '${analysisGroup.name}' already exists`);
|
||||
continue;
|
||||
@@ -71,19 +63,18 @@ async function createProductCategories({ medusa }: { medusa: Medusa }) {
|
||||
analysisGroupId: analysisGroup.id,
|
||||
},
|
||||
});
|
||||
console.info(
|
||||
`Successfully created category, id=${createResponse.product_category.id}`,
|
||||
);
|
||||
console.info(`Successfully created category, id=${createResponse.product_category.id}`);
|
||||
createdCategories.push(createResponse.product_category);
|
||||
}
|
||||
}
|
||||
|
||||
async function getChildProductCategories({ medusa }: { medusa: Medusa }) {
|
||||
const { product_categories: allCategories } =
|
||||
await medusa.admin.productCategory.list();
|
||||
const childCategories = allCategories.filter(
|
||||
({ parent_category_id }) => parent_category_id !== null,
|
||||
);
|
||||
async function getChildProductCategories({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
|
||||
const childCategories = allCategories.filter(({ parent_category_id }) => parent_category_id !== null);
|
||||
return childCategories;
|
||||
}
|
||||
|
||||
@@ -102,34 +93,34 @@ async function deleteProductCategories({
|
||||
/**
|
||||
* In case a reset is needed
|
||||
*/
|
||||
async function deleteProducts({ medusa }: { medusa: Medusa }) {
|
||||
async function deleteProducts({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { products: existingProducts } = await medusa.admin.product.list({
|
||||
fields: 'id,collection_id',
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
existingProducts
|
||||
.filter((a) => !a.collection_id)
|
||||
.map(({ id }) => medusa.admin.product.delete(id)),
|
||||
);
|
||||
|
||||
await Promise.all(existingProducts.filter((a) => !a.collection_id).map(({ id }) => medusa.admin.product.delete(id)));
|
||||
}
|
||||
|
||||
async function getAnalysisPackagesType() {
|
||||
const { productTypes } = await listProductTypes();
|
||||
const analysisPackagesType = productTypes.find(
|
||||
({ metadata }) => metadata?.handle === SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE,
|
||||
);
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE);
|
||||
if (!analysisPackagesType) {
|
||||
throw new Error('Synlab analysis packages type not found');
|
||||
}
|
||||
return analysisPackagesType;
|
||||
}
|
||||
|
||||
async function getProductDefaultFields({ medusa }: { medusa: Medusa }) {
|
||||
const baseProductsResponse = await medusa.admin.product.list({
|
||||
handle: BASE_ANALYSIS_PRODUCT_HANDLE,
|
||||
});
|
||||
async function getProductDefaultFields({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const baseProductsResponse = await medusa.admin.product.list({ handle: BASE_ANALYSIS_PRODUCT_HANDLE })
|
||||
const baseProduct = baseProductsResponse.products[0];
|
||||
if (!baseProduct) {
|
||||
throw new Error('Base product not found');
|
||||
@@ -151,36 +142,39 @@ async function getProductDefaultFields({ medusa }: { medusa: Medusa }) {
|
||||
defaultSalesChannels,
|
||||
defaultProductOption,
|
||||
defaultProductVariant,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function createProducts({ medusa }: { medusa: Medusa }) {
|
||||
const { product_categories: allCategories } =
|
||||
await medusa.admin.productCategory.list();
|
||||
async function createProducts({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
|
||||
|
||||
const [
|
||||
{ products: existingProducts },
|
||||
analysisElements,
|
||||
analysisPackagesType,
|
||||
{ defaultSalesChannels, defaultProductOption, defaultProductVariant },
|
||||
{
|
||||
defaultSalesChannels,
|
||||
defaultProductOption,
|
||||
defaultProductVariant,
|
||||
}
|
||||
] = await Promise.all([
|
||||
medusa.admin.product.list({
|
||||
category_id: allCategories.map(({ id }) => id),
|
||||
}),
|
||||
getAnalysisElements({ getAll: true }),
|
||||
getAnalysisElements({}),
|
||||
getAnalysisPackagesType(),
|
||||
getProductDefaultFields({ medusa }),
|
||||
]);
|
||||
])
|
||||
|
||||
for (const analysisElement of analysisElements) {
|
||||
const { analysis_id_original: originalId } = analysisElement;
|
||||
const isExisting = existingProducts.find(
|
||||
({ metadata }) => metadata?.analysisIdOriginal === originalId,
|
||||
);
|
||||
const isExisting = existingProducts.find(({ metadata }) => metadata?.analysisIdOriginal === originalId);
|
||||
if (isExisting) {
|
||||
console.info(
|
||||
`Analysis element '${analysisElement.analysis_name_lab}' already exists`,
|
||||
);
|
||||
console.info(`Analysis element '${analysisElement.analysis_name_lab}' already exists`);
|
||||
continue;
|
||||
}
|
||||
const { analysis_name_lab: name } = analysisElement;
|
||||
@@ -189,10 +183,7 @@ async function createProducts({ medusa }: { medusa: Medusa }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = allCategories.find(
|
||||
({ metadata }) =>
|
||||
metadata?.analysisGroupId === analysisElement.parent_analysis_group_id,
|
||||
);
|
||||
const category = allCategories.find(({ metadata }) => metadata?.analysisGroupId === analysisElement.parent_analysis_group_id);
|
||||
if (!category) {
|
||||
console.error(`Category not found for analysis element '${name}'`);
|
||||
continue;
|
||||
@@ -236,9 +227,9 @@ export default async function syncAnalysisGroupsStore() {
|
||||
// await deleteProductCategories({ medusa, categories });
|
||||
// await deleteProducts({ medusa });
|
||||
// return;
|
||||
|
||||
|
||||
await createProducts({ medusa });
|
||||
|
||||
|
||||
await createMedusaSyncSuccessEntry();
|
||||
} catch (e) {
|
||||
await createMedusaSyncFailEntry(JSON.stringify(e));
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import fs from 'fs';
|
||||
|
||||
import { toArray } from '@kit/shared/utils';
|
||||
|
||||
import {
|
||||
createAnalysis,
|
||||
createNoDataReceivedEntry,
|
||||
createNoNewDataReceivedEntry,
|
||||
createSyncFailEntry,
|
||||
createSyncSuccessEntry,
|
||||
getAnalyses,
|
||||
} from '~/lib/services/analyses.service';
|
||||
import {
|
||||
AnalysisElement,
|
||||
createAnalysisElement,
|
||||
getAnalysisElements,
|
||||
} from '~/lib/services/analysis-element.service';
|
||||
import {
|
||||
createAnalysisGroup,
|
||||
getAnalysisGroups,
|
||||
} from '~/lib/services/analysis-group.service';
|
||||
import { createCodes } from '~/lib/services/codes.service';
|
||||
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types';
|
||||
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
|
||||
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
||||
import { IMedipostPublicMessageDataParsed, IUuringElement } from '~/lib/services/medipost.types';
|
||||
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service';
|
||||
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
|
||||
import { createAnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
|
||||
import { createCodes } from '~/lib/services/codes.service';
|
||||
import { getLatestPublicMessageListItem } from '~/lib/services/medipost.service';
|
||||
import type { ICode } from '~/lib/types/code';
|
||||
|
||||
function toArray<T>(input?: T | T[] | null): T[] {
|
||||
if (!input) return [];
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
const WRITE_XML_TO_FILE = false as boolean;
|
||||
|
||||
export default async function syncAnalysisGroups() {
|
||||
@@ -41,7 +29,7 @@ export default async function syncAnalysisGroups() {
|
||||
|
||||
try {
|
||||
console.info('Getting latest public message id');
|
||||
// const lastCheckedDate = await getLastCheckedDate(); never used?
|
||||
const lastCheckedDate = await getLastCheckedDate();
|
||||
|
||||
const latestMessage = await getLatestPublicMessageListItem();
|
||||
if (!latestMessage) {
|
||||
@@ -68,8 +56,7 @@ export default async function syncAnalysisGroups() {
|
||||
}
|
||||
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed: IMedipostPublicMessageDataParsed =
|
||||
parser.parse(publicMessageData);
|
||||
const parsed: IMedipostPublicMessageDataParsed = parser.parse(publicMessageData);
|
||||
|
||||
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
|
||||
throw new Error(
|
||||
@@ -93,21 +80,15 @@ export default async function syncAnalysisGroups() {
|
||||
}
|
||||
|
||||
const codes: ICode[] = [];
|
||||
const analysesToCreate: { analysisGroupId: number, analyses: IUuringElement[], analysisElementId: number }[] = [];
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
const existingAnalysisGroup = existingAnalysisGroups?.find(
|
||||
({ original_id }) => original_id === analysisGroup.UuringuGruppId,
|
||||
);
|
||||
let groupExistingAnalysisElements: AnalysisElement[] = [];
|
||||
let analysisGroupId: number;
|
||||
let analysisGroupId: number | undefined;
|
||||
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
|
||||
if (existingAnalysisGroup) {
|
||||
console.info(
|
||||
`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`,
|
||||
);
|
||||
groupExistingAnalysisElements = await getAnalysisElements({
|
||||
analysisGroupId: existingAnalysisGroup.id,
|
||||
});
|
||||
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`);
|
||||
analysisGroupId = existingAnalysisGroup.id;
|
||||
} else {
|
||||
// SAVE ANALYSIS GROUP
|
||||
analysisGroupId = await createAnalysisGroup({
|
||||
id: analysisGroup.UuringuGruppId,
|
||||
name: analysisGroup.UuringuGruppNimi,
|
||||
@@ -121,7 +102,7 @@ export default async function syncAnalysisGroups() {
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: analysisGroupId,
|
||||
analysis_group_id: analysisGroupId!,
|
||||
analysis_element_id: null,
|
||||
analysis_id: null,
|
||||
})),
|
||||
@@ -131,87 +112,60 @@ export default async function syncAnalysisGroups() {
|
||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||
|
||||
for (const item of analysisGroupItems) {
|
||||
const analysisElement = item.UuringuElement!;
|
||||
const isExistingAnalysisElement = groupExistingAnalysisElements.find(
|
||||
({ analysis_id_original }) =>
|
||||
analysis_id_original === analysisElement.UuringId,
|
||||
);
|
||||
if (isExistingAnalysisElement) {
|
||||
console.info(
|
||||
`Analysis element '${analysisElement.UuringNimi}' already exists`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const analysisElement = item.UuringuElement;
|
||||
|
||||
const insertedAnalysisElementId = await createAnalysisElement({
|
||||
analysisElement: analysisElement!,
|
||||
analysisGroupId,
|
||||
materialGroups: toArray(item.MaterjalideGrupp),
|
||||
});
|
||||
|
||||
if (Array.isArray(analysisElement.UuringuElement)) {
|
||||
for (const nestedAnalysisElement of analysisElement.UuringuElement) {
|
||||
await createAnalysisElement({
|
||||
analysisElement: nestedAnalysisElement,
|
||||
analysisGroupId,
|
||||
materialGroups: toArray(item.MaterjalideGrupp),
|
||||
});
|
||||
let insertedAnalysisElementId: number | undefined;
|
||||
const existingAnalysisElement = (await getAnalysisElements({ originalIds: [analysisElement.UuringId] }))?.[0];
|
||||
if (existingAnalysisElement) {
|
||||
console.info(`Analysis element '${analysisElement.UuringNimi}' already exists`);
|
||||
insertedAnalysisElementId = existingAnalysisElement.id;
|
||||
} else {
|
||||
insertedAnalysisElementId = await createAnalysisElement({
|
||||
analysisElement,
|
||||
analysisGroupId: analysisGroupId!,
|
||||
materialGroups: toArray(item.MaterjalideGrupp),
|
||||
});
|
||||
if (analysisElement.Kood) {
|
||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
||||
codes.push(
|
||||
...analysisElementCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: insertedAnalysisElementId!,
|
||||
analysis_id: null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (analysisElement.Kood) {
|
||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
||||
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);
|
||||
|
||||
codes.push(
|
||||
...analysisElementCodes.map((kood) => ({
|
||||
...analysisCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: insertedAnalysisElementId,
|
||||
analysis_id: null,
|
||||
analysis_element_id: null,
|
||||
analysis_id: insertedAnalysisId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const analyses = analysisElement.UuringuElement;
|
||||
if (analyses?.length) {
|
||||
const existingAnalyses = await getAnalyses({
|
||||
originalIds: analyses.map(({ UuringId }) => UuringId),
|
||||
});
|
||||
|
||||
for (const analysis of analyses) {
|
||||
const isExistingAnalysis = existingAnalyses.find(
|
||||
({ analysis_id_original }) =>
|
||||
analysis_id_original === analysis.UuringId,
|
||||
);
|
||||
if (isExistingAnalysis) {
|
||||
console.info(`Analysis '${analysis.UuringNimi}' already exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,17 +176,11 @@ export default async function syncAnalysisGroups() {
|
||||
await createSyncSuccessEntry();
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
await createSyncFailEntry(
|
||||
JSON.stringify(
|
||||
{
|
||||
message: errorMessage,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
name: e instanceof Error ? e.name : 'Unknown',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
await createSyncFailEntry(JSON.stringify({
|
||||
message: errorMessage,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
name: e instanceof Error ? e.name : 'Unknown',
|
||||
}, null, 2));
|
||||
console.error('Sync failed:', e);
|
||||
throw new Error(
|
||||
`Failed to sync public message data, error: ${errorMessage}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MedipostPrivateMessageSync from '~/lib/services/medipost/medipostPrivateMessageSync.service';
|
||||
import { readPrivateMessageResponse } from "~/lib/services/medipost.service";
|
||||
|
||||
type ProcessedMessage = {
|
||||
messageId: string;
|
||||
@@ -6,46 +6,45 @@ type ProcessedMessage = {
|
||||
hasPartialAnalysisResponse: boolean;
|
||||
hasFullAnalysisResponse: boolean;
|
||||
medusaOrderId: string | undefined;
|
||||
analysisOrderId: number | undefined;
|
||||
};
|
||||
|
||||
type GroupedResults = {
|
||||
processed: Pick<ProcessedMessage, 'messageId' | 'analysisOrderId'>[];
|
||||
waitingForResults: Pick<ProcessedMessage, 'messageId' | 'analysisOrderId'>[];
|
||||
processed: Pick<ProcessedMessage, 'messageId' | 'medusaOrderId'>[];
|
||||
waitingForResults: Pick<ProcessedMessage, 'messageId' | 'medusaOrderId'>[];
|
||||
};
|
||||
|
||||
export default async function syncAnalysisResults() {
|
||||
console.info('Syncing analysis results');
|
||||
const sync = new MedipostPrivateMessageSync();
|
||||
|
||||
const processedMessages: ProcessedMessage[] = [];
|
||||
console.info("Syncing analysis results");
|
||||
|
||||
let processedMessages: ProcessedMessage[] = [];
|
||||
const excludedMessageIds: string[] = [];
|
||||
while (true) {
|
||||
const result = await sync.handleNextPrivateMessage({ excludedMessageIds });
|
||||
const result = await readPrivateMessageResponse({ excludedMessageIds });
|
||||
if (result.messageId) {
|
||||
processedMessages.push(result as ProcessedMessage);
|
||||
}
|
||||
|
||||
const { messageId } = result;
|
||||
if (!messageId) {
|
||||
console.info('No more messages to process');
|
||||
if (!result.messageId) {
|
||||
console.info("No more messages to process");
|
||||
break;
|
||||
}
|
||||
|
||||
processedMessages.push(result as ProcessedMessage);
|
||||
if (!excludedMessageIds.includes(messageId)) {
|
||||
excludedMessageIds.push(messageId);
|
||||
if (!excludedMessageIds.includes(result.messageId)) {
|
||||
excludedMessageIds.push(result.messageId);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const groupedResults = processedMessages.reduce((acc, result) => {
|
||||
if (result.analysisOrderId) {
|
||||
if (result.medusaOrderId) {
|
||||
if (result.hasAnalysisResponse) {
|
||||
if (!acc.processed) {
|
||||
acc.processed = [];
|
||||
}
|
||||
acc.processed.push({
|
||||
messageId: result.messageId,
|
||||
analysisOrderId: result.analysisOrderId,
|
||||
medusaOrderId: result.medusaOrderId,
|
||||
});
|
||||
} else {
|
||||
if (!acc.waitingForResults) {
|
||||
@@ -53,13 +52,11 @@ export default async function syncAnalysisResults() {
|
||||
}
|
||||
acc.waitingForResults.push({
|
||||
messageId: result.messageId,
|
||||
analysisOrderId: result.analysisOrderId,
|
||||
medusaOrderId: result.medusaOrderId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {} as GroupedResults);
|
||||
console.info(
|
||||
`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`,
|
||||
);
|
||||
console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`);
|
||||
}
|
||||
|
||||
@@ -2,45 +2,10 @@ import axios from 'axios';
|
||||
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
import { logSyncResult } from '~/lib/services/audit.service';
|
||||
import { SyncStatus } from '~/lib/types/audit';
|
||||
import type {
|
||||
ISearchLoadResponse,
|
||||
P_JobTitleTranslation,
|
||||
} from '~/lib/types/connected-online';
|
||||
|
||||
function createTranslationMap(translations: P_JobTitleTranslation[]) {
|
||||
const result: Map<
|
||||
number,
|
||||
Map<number, { textEN: string; textRU: string; textET: string }>
|
||||
> = new Map();
|
||||
|
||||
for (const translation of translations) {
|
||||
const { ClinicID, TextET, TextEN, TextRU, SyncID } = translation;
|
||||
|
||||
if (!result.has(ClinicID)) {
|
||||
result.set(ClinicID, new Map());
|
||||
}
|
||||
|
||||
result.get(ClinicID)!.set(SyncID, {
|
||||
textET: TextET,
|
||||
textEN: TextEN,
|
||||
textRU: TextRU,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSpokenLanguages(spokenLanguages?: string) {
|
||||
if (!spokenLanguages || !spokenLanguages.length) return [];
|
||||
return spokenLanguages.split(',');
|
||||
}
|
||||
import type { ISearchLoadResponse } from '~/lib/types/connected-online';
|
||||
|
||||
export default async function syncConnectedOnline() {
|
||||
const isProd = !['test', 'localhost'].some((pathString) =>
|
||||
process.env.NEXT_PUBLIC_SITE_URL?.includes(pathString),
|
||||
);
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const baseUrl = process.env.CONNECTED_ONLINE_URL;
|
||||
|
||||
@@ -51,19 +16,14 @@ export default async function syncConnectedOnline() {
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
|
||||
try {
|
||||
const searchLoadResponse = await axios.post<{ d: string }>(
|
||||
`${baseUrl}/Search_Load`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
param: "{'Value':'|et|-1'}", // get all available services in Estonian
|
||||
const response = await axios.post<{ d: string }>(`${baseUrl}/Search_Load`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
);
|
||||
param: "{'Value':'|et|-1'}", // get all available services in Estonian
|
||||
});
|
||||
|
||||
const responseData: ISearchLoadResponse = JSON.parse(
|
||||
searchLoadResponse.data.d,
|
||||
);
|
||||
const responseData: ISearchLoadResponse = JSON.parse(response.data.d);
|
||||
|
||||
if (responseData?.ErrorCode !== 0) {
|
||||
throw new Error('Failed to get Connected Online data');
|
||||
@@ -81,23 +41,20 @@ export default async function syncConnectedOnline() {
|
||||
});
|
||||
}
|
||||
|
||||
let clinics;
|
||||
let services;
|
||||
// Filter out "Dentas Demo OÜ" in prod or only sync "Dentas Demo OÜ" in any other environment
|
||||
const isDemoClinic = (clinicId: number) =>
|
||||
isProd ? clinicId !== 2 : clinicId === 2;
|
||||
const clinics = responseData.Data.T_Lic.filter(({ ID }) =>
|
||||
isDemoClinic(ID),
|
||||
);
|
||||
const services = responseData.Data.T_Service.filter(({ ClinicID }) =>
|
||||
isDemoClinic(ClinicID),
|
||||
);
|
||||
const serviceProviders = responseData.Data.T_Doctor.filter(({ ClinicID }) =>
|
||||
isDemoClinic(ClinicID),
|
||||
);
|
||||
const jobTitleTranslations = createTranslationMap(
|
||||
responseData.Data.P_JobTitleTranslations.filter(({ ClinicID }) =>
|
||||
isDemoClinic(ClinicID),
|
||||
),
|
||||
);
|
||||
if (isProd) {
|
||||
clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID !== 2);
|
||||
services = responseData.Data.T_Service.filter(
|
||||
(service) => service.ClinicID !== 2,
|
||||
);
|
||||
} else {
|
||||
clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID === 2);
|
||||
services = responseData.Data.T_Service.filter(
|
||||
(service) => service.ClinicID === 2,
|
||||
);
|
||||
}
|
||||
|
||||
const mappedClinics = clinics.map((clinic) => {
|
||||
return {
|
||||
@@ -107,8 +64,6 @@ export default async function syncConnectedOnline() {
|
||||
name: clinic.Name,
|
||||
personal_code_required: !!clinic.PersonalCodeRequired,
|
||||
phone_number: clinic.Phone || null,
|
||||
key: clinic.Key,
|
||||
address: clinic.Address,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -116,7 +71,7 @@ export default async function syncConnectedOnline() {
|
||||
return {
|
||||
id: service.ID,
|
||||
clinic_id: service.ClinicID,
|
||||
sync_id: Number(service.SyncID),
|
||||
sync_id: service.SyncID,
|
||||
name: service.Name,
|
||||
description: service.Description || null,
|
||||
price: service.Price,
|
||||
@@ -132,133 +87,45 @@ export default async function syncConnectedOnline() {
|
||||
};
|
||||
});
|
||||
|
||||
const mappedServiceProviders = serviceProviders.map((serviceProvider) => {
|
||||
const jobTitleTranslation = serviceProvider.JobTitleID
|
||||
? jobTitleTranslations
|
||||
.get(serviceProvider.ClinicID)
|
||||
?.get(serviceProvider.JobTitleID)
|
||||
: null;
|
||||
return {
|
||||
id: serviceProvider.ID,
|
||||
prefix: serviceProvider.Prefix,
|
||||
name: serviceProvider.Name,
|
||||
spoken_languages: getSpokenLanguages(serviceProvider.SpokenLanguages),
|
||||
job_title_et: jobTitleTranslation?.textET,
|
||||
job_title_en: jobTitleTranslation?.textEN,
|
||||
job_title_ru: jobTitleTranslation?.textRU,
|
||||
job_title_id: serviceProvider.JobTitleID,
|
||||
is_deleted: !!serviceProvider.Deleted,
|
||||
clinic_id: serviceProvider.ClinicID,
|
||||
};
|
||||
});
|
||||
|
||||
const { error: providersError } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_providers')
|
||||
.upsert(mappedClinics);
|
||||
|
||||
if (providersError) {
|
||||
return logSyncResult({
|
||||
operation: 'CONNECTED_ONLINE_SYNC',
|
||||
comment:
|
||||
'Error saving connected online providers: ' +
|
||||
JSON.stringify(providersError),
|
||||
status: SyncStatus.Fail,
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
const { error: servicesError } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_services')
|
||||
.upsert(mappedServices, {
|
||||
onConflict: 'id',
|
||||
ignoreDuplicates: false,
|
||||
});
|
||||
.upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false });
|
||||
|
||||
if (servicesError) {
|
||||
return logSyncResult({
|
||||
operation: 'CONNECTED_ONLINE_SYNC',
|
||||
comment:
|
||||
'Error saving connected online services: ' +
|
||||
JSON.stringify(servicesError),
|
||||
status: SyncStatus.Fail,
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
if (providersError || servicesError) {
|
||||
return supabase
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.insert({
|
||||
operation: 'CONNECTED_ONLINE_SYNC',
|
||||
comment: providersError
|
||||
? 'Error saving providers: ' + JSON.stringify(providersError)
|
||||
: 'Error saving services: ' + JSON.stringify(servicesError),
|
||||
status: 'FAIL',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
const { error: serviceProvidersError } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_service_providers')
|
||||
.upsert(mappedServiceProviders, {
|
||||
onConflict: 'id',
|
||||
ignoreDuplicates: false,
|
||||
});
|
||||
|
||||
if (serviceProvidersError) {
|
||||
return logSyncResult({
|
||||
operation: 'CONNECTED_ONLINE_SYNC',
|
||||
comment:
|
||||
'Error saving service providers: ' +
|
||||
JSON.stringify(serviceProvidersError),
|
||||
status: SyncStatus.Fail,
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
for (const mappedClinic of mappedClinics) {
|
||||
const defaultLoadResponse = await axios.post<{ d: string }>(
|
||||
`${baseUrl}/Default_Load`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
param: `{'Value':'${mappedClinic.key}|et'}`,
|
||||
},
|
||||
);
|
||||
|
||||
const defaultLoadResponseData = JSON.parse(defaultLoadResponse.data.d);
|
||||
|
||||
if (defaultLoadResponseData?.ErrorCode !== 0) {
|
||||
throw new Error('Failed to get Connected Online location data');
|
||||
}
|
||||
|
||||
const clinicLocations: {
|
||||
SyncID: number;
|
||||
Address: string;
|
||||
Name: string;
|
||||
}[] = defaultLoadResponseData.Data.T_SelectableLocation;
|
||||
|
||||
if (clinicLocations?.length) {
|
||||
const mappedLocations = clinicLocations.map(
|
||||
({ SyncID, Address, Name }) => ({
|
||||
address: Address,
|
||||
clinic_id: mappedClinic.id,
|
||||
sync_id: SyncID,
|
||||
name: Name,
|
||||
}),
|
||||
);
|
||||
|
||||
await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_locations')
|
||||
.insert(mappedLocations)
|
||||
.throwOnError();
|
||||
}
|
||||
}
|
||||
|
||||
await logSyncResult({
|
||||
await supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'CONNECTED_ONLINE_SYNC',
|
||||
status: SyncStatus.Success,
|
||||
status: 'SUCCESS',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
} catch (e) {
|
||||
await logSyncResult({
|
||||
operation: 'CONNECTED_ONLINE_SYNC',
|
||||
status: SyncStatus.Fail,
|
||||
comment: JSON.stringify(e),
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
await supabase
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.insert({
|
||||
operation: 'CONNECTED_ONLINE_SYNC',
|
||||
status: 'FAIL',
|
||||
comment: JSON.stringify(e),
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
throw new Error(
|
||||
`Failed to sync Connected Online data, error: ${JSON.stringify(e)}`,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export default function validateApiKey(request: NextRequest) {
|
||||
const envApiKey = process.env.JOBS_API_TOKEN;
|
||||
|
||||
@@ -1,60 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getLogger } from '@/packages/shared/src/logger';
|
||||
import { retrieveOrder } from '@lib/data/orders';
|
||||
|
||||
import { getMedipostDispatchTries } from '~/lib/services/audit.service';
|
||||
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
||||
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||
|
||||
import loadEnv from '../handler/load-env';
|
||||
import validateApiKey from '../handler/validate-api-key';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
|
||||
import { retrieveOrder } from "@lib/data/orders";
|
||||
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const logger = await getLogger();
|
||||
const ctx = {
|
||||
api: '/job/medipost-retry-dispatch',
|
||||
};
|
||||
loadEnv();
|
||||
|
||||
const { medusaOrderId } = await request.json();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const tries = await getMedipostDispatchTries(medusaOrderId);
|
||||
if (tries >= 3) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Order has been retried too many times',
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
return NextResponse.json({
|
||||
message: 'Order has been retried too many times',
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId);
|
||||
const orderedAnalysisElements = await getOrderedAnalysisIds({
|
||||
medusaOrder,
|
||||
});
|
||||
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
|
||||
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||
logger.info(ctx, 'Successfully sent order to medipost');
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Successfully sent order to medipost',
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error({ ...ctx, error }, 'Error sending order to medipost');
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Failed to send order to medipost',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
console.info("Successfully sent order to medipost");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully sent order to medipost',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error("Error sending order to medipost", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to send order to medipost',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
NotificationAction,
|
||||
createNotificationLog,
|
||||
} from '~/lib/services/audit/notificationEntries.service';
|
||||
|
||||
import loadEnv from '../handler/load-env';
|
||||
import sendOpenJobsEmails from '../handler/send-open-jobs-emails';
|
||||
import validateApiKey from '../handler/validate-api-key';
|
||||
@@ -14,19 +13,18 @@ export const POST = async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const doctors = await sendOpenJobsEmails();
|
||||
await sendOpenJobsEmails();
|
||||
console.info(
|
||||
'Successfully sent out open job notification emails to doctors',
|
||||
'Successfully sent out open job notification emails to doctors.',
|
||||
);
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_NEW_JOBS,
|
||||
action: NotificationAction.NEW_JOBS_ALERT,
|
||||
status: 'SUCCESS',
|
||||
comment: `doctors that received email: ${doctors}`,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -41,7 +39,7 @@ export const POST = async (request: NextRequest) => {
|
||||
e,
|
||||
);
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.DOCTOR_NEW_JOBS,
|
||||
action: NotificationAction.NEW_JOBS_ALERT,
|
||||
status: 'FAIL',
|
||||
comment: e?.message,
|
||||
});
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import loadEnv from '../handler/load-env';
|
||||
import syncAnalysisGroupsStore from '../handler/sync-analysis-groups-store';
|
||||
import validateApiKey from '../handler/validate-api-key';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import syncAnalysisGroupsStore from "../handler/sync-analysis-groups-store";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncAnalysisGroupsStore();
|
||||
console.info('Successfully synced analysis groups store');
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Successfully synced analysis groups store',
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
console.info("Successfully synced analysis groups store");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced analysis groups store',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error('Error syncing analysis groups store', e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Failed to sync analysis groups store',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
console.error("Error syncing analysis groups store", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync analysis groups store',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import loadEnv from '../handler/load-env';
|
||||
import syncAnalysisGroups from '../handler/sync-analysis-groups';
|
||||
import validateApiKey from '../handler/validate-api-key';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import syncAnalysisGroups from "../handler/sync-analysis-groups";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncAnalysisGroups();
|
||||
console.info('Successfully synced analysis groups');
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Successfully synced analysis groups',
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
console.info("Successfully synced analysis groups");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced analysis groups',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error('Error syncing analysis groups', e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Failed to sync analysis groups',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
console.error("Error syncing analysis groups", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync analysis groups',
|
||||
error: e instanceof Error ? JSON.stringify(e, undefined, 2) : 'Unknown error',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import loadEnv from '../handler/load-env';
|
||||
import syncAnalysisResults from '../handler/sync-analysis-results';
|
||||
import validateApiKey from '../handler/validate-api-key';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import syncAnalysisResults from "../handler/sync-analysis-results";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncAnalysisResults();
|
||||
console.info('Successfully synced analysis results');
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Successfully synced analysis results',
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
console.info("Successfully synced analysis results");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced analysis results',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error('Error syncing analysis results', e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Failed to sync analysis results',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
console.error("Error syncing analysis results", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync analysis results',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import loadEnv from '../handler/load-env';
|
||||
import syncConnectedOnline from '../handler/sync-connected-online';
|
||||
import validateApiKey from '../handler/validate-api-key';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import syncConnectedOnline from "../handler/sync-connected-online";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncConnectedOnline();
|
||||
console.info('Successfully synced connected-online');
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Successfully synced connected-online',
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
console.info("Successfully synced connected-online");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced connected-online',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error('Error syncing connected-online', e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Failed to sync connected-online',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
console.error("Error syncing connected-online", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync connected-online',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { retrieveOrder } from '@lib/data';
|
||||
|
||||
import { getAccountAdmin } from '~/lib/services/account.service';
|
||||
import {
|
||||
composeOrderTestResponseXML,
|
||||
sendPrivateMessageTestResponse,
|
||||
} from '~/lib/services/medipost/medipostTest.service';
|
||||
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||
import { getAnalysisOrdersAdmin } from '~/lib/services/order.service';
|
||||
|
||||
import loadEnv from '../handler/load-env';
|
||||
import validateApiKey from '../handler/validate-api-key';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
|
||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
|
||||
import { retrieveOrder } from "@lib/data";
|
||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
||||
import { getOrderedAnalysisIds } from "~/lib/services/medipost.service";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const analysisOrders = await getAnalysisOrdersAdmin({
|
||||
orderStatus: 'PROCESSING',
|
||||
});
|
||||
const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'PROCESSING' });
|
||||
|
||||
console.error(
|
||||
`Sending test responses for ${analysisOrders.length} analysis orders`,
|
||||
);
|
||||
console.error(`Sending test responses for ${analysisOrders.length} analysis orders`);
|
||||
for (const medreportOrder of analysisOrders) {
|
||||
const medusaOrderId = medreportOrder.medusa_order_id;
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId);
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
||||
|
||||
const account = await getAccountAdmin({
|
||||
primaryOwnerUserId: medreportOrder.user_id,
|
||||
});
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({
|
||||
medusaOrder,
|
||||
});
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
|
||||
|
||||
console.info(
|
||||
`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`,
|
||||
);
|
||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
||||
const idsToSend = orderedAnalysisElementsIds;
|
||||
const messageXml = await composeOrderTestResponseXML({
|
||||
person: {
|
||||
@@ -51,20 +35,16 @@ export async function POST(request: NextRequest) {
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: idsToSend
|
||||
.map(({ analysisElementId }) => analysisElementId)
|
||||
.filter(Boolean) as number[],
|
||||
orderedAnalysesIds: idsToSend
|
||||
.map(({ analysisId }) => analysisId)
|
||||
.filter(Boolean) as number[],
|
||||
orderId: medreportOrder.id,
|
||||
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
||||
orderedAnalysesIds: idsToSend.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
});
|
||||
|
||||
try {
|
||||
await sendPrivateMessageTestResponse({ messageXml });
|
||||
} catch (error) {
|
||||
console.error('Error sending private message test response: ', error);
|
||||
console.error("Error sending private message test response: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -95,4 +94,4 @@ export const POST = enhanceRouteHandler(
|
||||
{
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
);
|
||||
@@ -1,15 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { retrieveOrder } from '@lib/data';
|
||||
|
||||
import { getAccountAdmin } from '~/lib/services/account.service';
|
||||
import { upsertMedipostActionLog } from '~/lib/services/medipost/medipostMessageBase.service';
|
||||
import {
|
||||
composeOrderTestResponseXML,
|
||||
sendPrivateMessageTestResponse,
|
||||
} from '~/lib/services/medipost/medipostTest.service';
|
||||
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||
import { getAnalysisOrder } from '~/lib/services/order.service';
|
||||
import { NextResponse } from "next/server";
|
||||
import { getOrder } from "~/lib/services/order.service";
|
||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
|
||||
import { retrieveOrder } from "@lib/data";
|
||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
||||
import { createMedipostActionLog, getOrderedAnalysisIds } from "~/lib/services/medipost.service";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// const isDev = process.env.NODE_ENV === 'development';
|
||||
@@ -19,19 +13,13 @@ export async function POST(request: Request) {
|
||||
|
||||
const { medusaOrderId } = await request.json();
|
||||
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId);
|
||||
const analysisOrder = await getAnalysisOrder({ medusaOrderId });
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
||||
const medreportOrder = await getOrder({ medusaOrderId });
|
||||
|
||||
const account = await getAccountAdmin({
|
||||
primaryOwnerUserId: analysisOrder.user_id,
|
||||
});
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({
|
||||
medusaOrder,
|
||||
});
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
|
||||
|
||||
console.info(
|
||||
`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`,
|
||||
);
|
||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
||||
const messageXml = await composeOrderTestResponseXML({
|
||||
person: {
|
||||
idCode: account.personal_code!,
|
||||
@@ -39,26 +27,21 @@ export async function POST(request: Request) {
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: orderedAnalysisElementsIds
|
||||
.map(({ analysisElementId }) => analysisElementId)
|
||||
.filter(Boolean) as number[],
|
||||
orderedAnalysesIds: orderedAnalysisElementsIds
|
||||
.map(({ analysisId }) => analysisId)
|
||||
.filter(Boolean) as number[],
|
||||
orderId: analysisOrder.id,
|
||||
orderCreatedAt: new Date(analysisOrder.created_at),
|
||||
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
||||
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
});
|
||||
|
||||
try {
|
||||
await upsertMedipostActionLog({
|
||||
await createMedipostActionLog({
|
||||
action: 'send_fake_analysis_results_to_medipost',
|
||||
xml: messageXml,
|
||||
medusaOrderId,
|
||||
medipostPrivateMessageId: `fake-response-${Date.now()}`,
|
||||
});
|
||||
await sendPrivateMessageTestResponse({ messageXml });
|
||||
} catch (error) {
|
||||
console.error('Error sending private message test response: ', error);
|
||||
console.error("Error sending private message test response: ", error);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
|
||||
@@ -3,13 +3,15 @@ import Link from 'next/link';
|
||||
import type { AuthError } from '@supabase/supabase-js';
|
||||
|
||||
import { ResendAuthLinkForm } from '@kit/auth/resend-email-link';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
interface AuthCallbackErrorPageProps {
|
||||
searchParams: Promise<{
|
||||
error: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
||||
import { createAuthCallbackService, getErrorURLParameters } from '@kit/supabase/auth';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import {
|
||||
createAuthCallbackService,
|
||||
getErrorURLParameters,
|
||||
} from '@kit/supabase/auth';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
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) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
@@ -47,22 +44,19 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const service = createAuthCallbackService(getSupabaseServerClient());
|
||||
const oauthResult = await service.exchangeCodeForSession(authCode);
|
||||
|
||||
if (oauthResult.requiresMultiFactorAuthentication) {
|
||||
redirect(pathsConfig.auth.verifyMfa);
|
||||
}
|
||||
|
||||
if (!('isSuccess' in oauthResult)) {
|
||||
if (!("isSuccess" in oauthResult)) {
|
||||
return redirectOnError(oauthResult.searchParams);
|
||||
}
|
||||
|
||||
const api = createAccountsApi(getSupabaseServerClient());
|
||||
|
||||
const account = await api.getPersonalAccountByUserId(oauthResult.user.id);
|
||||
const account = await api.getPersonalAccountByUserId(
|
||||
oauthResult.user.id,
|
||||
);
|
||||
|
||||
if (!account.email || !account.name || !account.last_name) {
|
||||
return redirect(pathsConfig.auth.updateAccount);
|
||||
}
|
||||
|
||||
|
||||
return redirect(redirectPath);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { createAuthCallbackService } from '@kit/supabase/auth';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const service = createAuthCallbackService(getSupabaseServerClient());
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { SuccessNotification } from '@kit/notifications/components';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
const MembershipConfirmationNotification: React.FC<{
|
||||
userId: string;
|
||||
@@ -25,7 +25,7 @@ const MembershipConfirmationNotification: React.FC<{
|
||||
descriptionKey="account:membershipConfirmation:successDescription"
|
||||
buttonProps={{
|
||||
buttonTitleKey: 'account:membershipConfirmation:successButton',
|
||||
href: pathsConfig.app.selectPackage,
|
||||
href: pathsConfig.app.home,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
async function SiteLayout(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { PasswordResetRequestContainer } from '@kit/auth/password-reset';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Providers, SignInMethodsContainer } from '@kit/auth/sign-in';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
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';
|
||||
@@ -9,11 +9,9 @@ import { Trans } from '@kit/ui/trans';
|
||||
export default function PasswordOption({
|
||||
inviteToken,
|
||||
returnPath,
|
||||
providers,
|
||||
}: {
|
||||
inviteToken?: string;
|
||||
returnPath?: string;
|
||||
providers: Providers;
|
||||
}) {
|
||||
const signUpPath =
|
||||
pathsConfig.auth.signUp +
|
||||
@@ -41,7 +39,7 @@ export default function PasswordOption({
|
||||
<SignInMethodsContainer
|
||||
inviteToken={inviteToken}
|
||||
paths={paths}
|
||||
providers={providers}
|
||||
providers={authConfig.providers}
|
||||
/>
|
||||
|
||||
<div className={'flex justify-center'}>
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
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({
|
||||
const { data, error } = await getSupabaseBrowserClient()
|
||||
.auth
|
||||
.signInWithOAuth({
|
||||
provider: 'keycloak',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`,
|
||||
queryParams: {
|
||||
prompt: 'login',
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { getServerAuthConfig, pathsConfig } from '@kit/shared/config';
|
||||
import { pathsConfig, authConfig } from '@kit/shared/config';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import PasswordOption from './components/PasswordOption';
|
||||
import { SignInPageClientRedirect } from './components/SignInPageClientRedirect';
|
||||
import PasswordOption from './components/PasswordOption';
|
||||
|
||||
interface SignInPageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -25,23 +24,11 @@ async function SignInPage({ searchParams }: SignInPageProps) {
|
||||
const { invite_token: inviteToken, next: returnPath = pathsConfig.app.home } =
|
||||
await searchParams;
|
||||
|
||||
const authConfig = await getServerAuthConfig();
|
||||
|
||||
if (authConfig.providers.password) {
|
||||
return (
|
||||
<PasswordOption
|
||||
inviteToken={inviteToken}
|
||||
returnPath={returnPath}
|
||||
providers={authConfig.providers}
|
||||
/>
|
||||
);
|
||||
return <PasswordOption inviteToken={inviteToken} returnPath={returnPath} />;
|
||||
}
|
||||
|
||||
if (authConfig.providers.oAuth.includes('keycloak')) {
|
||||
return <SignInPageClientRedirect />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return <SignInPageClientRedirect />;
|
||||
}
|
||||
|
||||
export default withI18n(SignInPage);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
@@ -2,12 +2,13 @@ import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
|
||||
import { getServerAuthConfig, pathsConfig } from '@kit/shared/config';
|
||||
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 { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
@@ -37,8 +38,6 @@ async function SignUpPage({ searchParams }: Props) {
|
||||
pathsConfig.auth.signIn +
|
||||
(inviteToken ? `?invite_token=${inviteToken}` : '');
|
||||
|
||||
const authConfig = await getServerAuthConfig();
|
||||
|
||||
if (!authConfig.providers.password) {
|
||||
return redirect('/');
|
||||
}
|
||||
@@ -56,7 +55,8 @@ async function SignUpPage({ searchParams }: Props) {
|
||||
</div>
|
||||
|
||||
<SignUpMethodsContainer
|
||||
authConfig={authConfig}
|
||||
providers={authConfig.providers}
|
||||
displayTermsCheckbox={authConfig.displayTermsCheckbox}
|
||||
inviteToken={inviteToken}
|
||||
paths={paths}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { pathsConfig } from '@/packages/shared/src/config';
|
||||
import { ExternalLink } from '@/public/assets/external-link';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Checkbox } from '@kit/ui/checkbox';
|
||||
@@ -21,80 +17,46 @@ import {
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { UpdateAccountSchemaClient } from '../_lib/schemas/update-account.schema';
|
||||
import { UpdateAccountSchema } from '../_lib/schemas/update-account.schema';
|
||||
import { onUpdateAccount } from '../_lib/server/update-account';
|
||||
import { z } from 'zod';
|
||||
|
||||
type UpdateAccountFormValues = z.infer<
|
||||
ReturnType<typeof UpdateAccountSchemaClient>
|
||||
>;
|
||||
type UpdateAccountFormValues = z.infer<typeof UpdateAccountSchema>;
|
||||
|
||||
export function UpdateAccountForm({
|
||||
defaultValues,
|
||||
isEmailUser,
|
||||
}: {
|
||||
defaultValues: UpdateAccountFormValues;
|
||||
isEmailUser: boolean;
|
||||
defaultValues: UpdateAccountFormValues,
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation('account');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(UpdateAccountSchemaClient({ isEmailUser })),
|
||||
resolver: zodResolver(UpdateAccountSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const { firstName, lastName, personalCode, email, userConsent } =
|
||||
defaultValues;
|
||||
|
||||
const defaultValues_weight =
|
||||
'weight' in defaultValues ? defaultValues.weight : null;
|
||||
const defaultValues_height =
|
||||
'height' in defaultValues ? defaultValues.height : null;
|
||||
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) => {
|
||||
const loading = toast.loading(t('updateAccount.updateAccountLoading'));
|
||||
try {
|
||||
const response = await onUpdateAccount({
|
||||
firstName: values.firstName || firstName,
|
||||
lastName: values.lastName || lastName,
|
||||
personalCode: values.personalCode || personalCode,
|
||||
email: values.email || email,
|
||||
phone: values.phone,
|
||||
weight: ((('weight' in values && values.weight) ??
|
||||
defaultValues_weight) ||
|
||||
null) as number,
|
||||
height: ((('height' in values && values.height) ??
|
||||
defaultValues_height) ||
|
||||
null) as number,
|
||||
userConsent: values.userConsent ?? userConsent,
|
||||
city: values.city,
|
||||
});
|
||||
if (!response) {
|
||||
throw new Error('Failed to update account');
|
||||
}
|
||||
toast.dismiss(loading);
|
||||
toast.success(t('updateAccount.updateAccountSuccess'));
|
||||
|
||||
if (response.hasUnseenMembershipConfirmation) {
|
||||
router.push(pathsConfig.auth.membershipConfirmation);
|
||||
} else {
|
||||
router.push(pathsConfig.app.selectPackage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.info('promiseresult error', error);
|
||||
toast.error(t('updateAccount.updateAccountError'));
|
||||
toast.dismiss(loading);
|
||||
}
|
||||
};
|
||||
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 (
|
||||
<Form {...form}>
|
||||
@@ -104,14 +66,14 @@ export function UpdateAccountForm({
|
||||
>
|
||||
<FormField
|
||||
name="firstName"
|
||||
disabled={hasFirstName && !isEmailUser}
|
||||
disabled={hasFirstName}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:firstName'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} autoFocus={!hasFirstName} />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -120,14 +82,14 @@ export function UpdateAccountForm({
|
||||
|
||||
<FormField
|
||||
name="lastName"
|
||||
disabled={hasLastName && !isEmailUser}
|
||||
disabled={hasLastName}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:lastName'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} autoFocus={hasFirstName && !hasLastName} />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -136,7 +98,7 @@ export function UpdateAccountForm({
|
||||
|
||||
<FormField
|
||||
name="personalCode"
|
||||
disabled={hasPersonalCode && !isEmailUser}
|
||||
disabled={hasPersonalCode}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
@@ -181,74 +143,72 @@ export function UpdateAccountForm({
|
||||
)}
|
||||
/>
|
||||
|
||||
<>
|
||||
<FormField
|
||||
name="city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:city'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-between gap-4">
|
||||
<FormField
|
||||
name="city"
|
||||
name="weight"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:city'} />
|
||||
<Trans i18nKey={'common:formField:weight'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="kg"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === '' ? null : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-between gap-4">
|
||||
<FormField
|
||||
name="weight"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 basis-0">
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:weight'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="kg"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === '' ? null : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="height"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 basis-0">
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:height'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="cm"
|
||||
type="number"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === '' ? null : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<FormField
|
||||
name="height"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField:height'} />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="cm"
|
||||
type="number"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === '' ? null : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
name="userConsent"
|
||||
@@ -267,7 +227,7 @@ export function UpdateAccountForm({
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={t('account:updateAccount:userConsentUrlPath')}
|
||||
href={''}
|
||||
className="flex flex-row items-center gap-2 text-sm hover:underline"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Isikukood from 'isikukood';
|
||||
import parsePhoneNumber from 'libphonenumber-js/min';
|
||||
import { z } from 'zod';
|
||||
import parsePhoneNumber from 'libphonenumber-js/min';
|
||||
|
||||
const updateAccountSchema = {
|
||||
export const UpdateAccountSchema = z.object({
|
||||
firstName: z
|
||||
.string({
|
||||
error: 'First name is required',
|
||||
@@ -12,45 +11,32 @@ const updateAccountSchema = {
|
||||
.string({
|
||||
error: 'Last name is required',
|
||||
})
|
||||
.nonempty({
|
||||
error: 'common:formFieldError.stringNonEmpty',
|
||||
}),
|
||||
personalCode: z.string().refine(
|
||||
(val) => {
|
||||
try {
|
||||
return new Isikukood(val).validate();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message: 'common:formFieldError.invalidPersonalCode',
|
||||
},
|
||||
),
|
||||
.nonempty(),
|
||||
personalCode: z
|
||||
.string({
|
||||
error: 'Personal code is required',
|
||||
})
|
||||
.nonempty(),
|
||||
email: z.string().email({
|
||||
message: 'Email is required',
|
||||
}),
|
||||
phone: z
|
||||
.string({
|
||||
error: 'error:invalidPhone',
|
||||
error: 'Phone number is required',
|
||||
})
|
||||
.nonempty()
|
||||
.refine(
|
||||
(phone) => {
|
||||
try {
|
||||
const phoneNumber = parsePhoneNumber(phone);
|
||||
return (
|
||||
!!phoneNumber &&
|
||||
phoneNumber.isValid() &&
|
||||
phoneNumber.country === 'EE'
|
||||
);
|
||||
return !!phoneNumber && phoneNumber.isValid() && phoneNumber.country === 'EE';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message: 'common:formFieldError.invalidPhoneNumber',
|
||||
},
|
||||
}
|
||||
),
|
||||
city: z.string().optional(),
|
||||
weight: z
|
||||
@@ -73,39 +59,4 @@ const updateAccountSchema = {
|
||||
userConsent: z.boolean().refine((val) => val === true, {
|
||||
message: 'Must be true',
|
||||
}),
|
||||
} as const;
|
||||
export const UpdateAccountSchemaServer = z.object({
|
||||
firstName: updateAccountSchema.firstName,
|
||||
lastName: updateAccountSchema.lastName,
|
||||
personalCode: updateAccountSchema.personalCode,
|
||||
email: updateAccountSchema.email,
|
||||
phone: updateAccountSchema.phone,
|
||||
city: updateAccountSchema.city,
|
||||
weight: updateAccountSchema.weight.nullable(),
|
||||
height: updateAccountSchema.height.nullable(),
|
||||
userConsent: updateAccountSchema.userConsent,
|
||||
});
|
||||
export const UpdateAccountSchemaClient = ({
|
||||
isEmailUser,
|
||||
}: {
|
||||
isEmailUser: boolean;
|
||||
}) =>
|
||||
z.object({
|
||||
firstName: updateAccountSchema.firstName,
|
||||
lastName: updateAccountSchema.lastName,
|
||||
personalCode: updateAccountSchema.personalCode,
|
||||
email: updateAccountSchema.email,
|
||||
phone: updateAccountSchema.phone,
|
||||
...(isEmailUser
|
||||
? {
|
||||
city: z.string().optional(),
|
||||
weight: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
}
|
||||
: {
|
||||
city: updateAccountSchema.city,
|
||||
weight: updateAccountSchema.weight,
|
||||
height: updateAccountSchema.height,
|
||||
}),
|
||||
userConsent: updateAccountSchema.userConsent,
|
||||
});
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
'use server';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { updateCustomer } from '@lib/data/customer';
|
||||
|
||||
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { UpdateAccountSchemaServer } from '../schemas/update-account.schema';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
|
||||
import { UpdateAccountSchema } from '../schemas/update-account.schema';
|
||||
|
||||
export const onUpdateAccount = enhanceAction(
|
||||
async (params: AccountSubmitData) => {
|
||||
@@ -15,6 +20,7 @@ export const onUpdateAccount = enhanceAction(
|
||||
|
||||
try {
|
||||
await api.updateAccount(params);
|
||||
console.log('SUCCESS', pathsConfig.auth.updateAccountSuccess);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
console.warn('On update account error: ' + err.message);
|
||||
@@ -29,16 +35,19 @@ export const onUpdateAccount = enhanceAction(
|
||||
phone: params.phone,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to update Medusa customer', e);
|
||||
console.error("Failed to update Medusa customer", e);
|
||||
}
|
||||
|
||||
const hasUnseenMembershipConfirmation =
|
||||
await api.hasUnseenMembershipConfirmation();
|
||||
return {
|
||||
hasUnseenMembershipConfirmation,
|
||||
};
|
||||
|
||||
if (hasUnseenMembershipConfirmation) {
|
||||
redirect(pathsConfig.auth.membershipConfirmation);
|
||||
} else {
|
||||
redirect(pathsConfig.app.selectPackage);
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: UpdateAccountSchemaServer,
|
||||
schema: UpdateAccountSchema,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
async function SiteLayout(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||
import { signOutAction } from '@/lib/actions/sign-out';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
import { BackButton } from '@kit/shared/components/back-button';
|
||||
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
||||
@@ -9,15 +9,19 @@ import { pathsConfig } from '@kit/shared/config';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { toTitleCase } from '~/lib/utils';
|
||||
|
||||
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() {
|
||||
const { account, user } = await loadCurrentUserAccount();
|
||||
const client = getSupabaseServerClient();
|
||||
const account = await loadCurrentUserAccount();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await client.auth.getUser();
|
||||
const isKeycloakUser = user?.app_metadata?.provider === 'keycloak';
|
||||
const isEmailUser = user?.app_metadata?.provider === 'email';
|
||||
|
||||
if (!user) {
|
||||
redirect(pathsConfig.auth.signIn);
|
||||
@@ -33,16 +37,16 @@ async function UpdateAccount() {
|
||||
}
|
||||
return account?.email ?? user?.email ?? '';
|
||||
})(),
|
||||
phone: account?.phone ?? '+372',
|
||||
phone: account?.phone ?? '',
|
||||
city: account?.city ?? '',
|
||||
weight: account?.accountParams?.weight ?? null,
|
||||
height: account?.accountParams?.height ?? null,
|
||||
weight: account?.accountParams?.weight ?? 0,
|
||||
height: account?.accountParams?.height ?? 0,
|
||||
userConsent: account?.has_consent_personal_data ?? false,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-border xs:w-3/5 flex w-9/10 flex-row overflow-hidden rounded-3xl border md:w-[988px]">
|
||||
<div className="xs:px-6 relative flex w-full flex-col pt-7 pb-22 text-center md:w-1/2 md:px-12">
|
||||
<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">
|
||||
<BackButton onBack={signOutAction} />
|
||||
<MedReportLogo />
|
||||
<h1 className="pt-8">
|
||||
@@ -51,12 +55,9 @@ async function UpdateAccount() {
|
||||
<p className="text-muted-foreground pt-1 text-sm">
|
||||
<Trans i18nKey={'account:updateAccount:description'} />
|
||||
</p>
|
||||
<UpdateAccountForm
|
||||
defaultValues={defaultValues}
|
||||
isEmailUser={isEmailUser}
|
||||
/>
|
||||
<UpdateAccountForm defaultValues={defaultValues} />
|
||||
</div>
|
||||
<div className="hidden w-1/2 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { MultiFactorChallengeContainer } from '@kit/auth/mfa';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
@@ -40,11 +41,15 @@ async function VerifyPage(props: Props) {
|
||||
}
|
||||
|
||||
const nextPath = (await props.searchParams).next;
|
||||
const redirectPath =
|
||||
!!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home;
|
||||
const redirectPath = !!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home;
|
||||
|
||||
return (
|
||||
<MultiFactorChallengeContainer userId={user.id} paths={{ redirectPath }} />
|
||||
<MultiFactorChallengeContainer
|
||||
userId={user.id}
|
||||
paths={{
|
||||
redirectPath,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { ReactElement, ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
|
||||
import { format } from 'date-fns';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { AnalysisElement } from '~/lib/services/analysis-element.service';
|
||||
|
||||
import AnalysisLevelBar, {
|
||||
AnalysisLevelBarSkeleton,
|
||||
AnalysisResultLevel,
|
||||
} from './analysis-level-bar';
|
||||
|
||||
export type AnalysisResultForDisplay = Pick<
|
||||
UserAnalysisElement,
|
||||
| 'norm_status'
|
||||
| 'response_value'
|
||||
| 'unit'
|
||||
| 'norm_lower_included'
|
||||
| 'norm_upper_included'
|
||||
| 'norm_lower'
|
||||
| 'norm_upper'
|
||||
| 'response_time'
|
||||
>;
|
||||
|
||||
export enum AnalysisStatus {
|
||||
NORMAL = 0,
|
||||
MEDIUM = 1,
|
||||
HIGH = 2,
|
||||
}
|
||||
|
||||
const AnalysisDoctor = ({
|
||||
analysisElement,
|
||||
results,
|
||||
startIcon,
|
||||
endIcon,
|
||||
isCancelled,
|
||||
}: {
|
||||
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
|
||||
results?: AnalysisResultForDisplay;
|
||||
isCancelled?: boolean;
|
||||
startIcon?: ReactElement | null;
|
||||
endIcon?: ReactNode | null;
|
||||
}) => {
|
||||
const name = analysisElement.analysis_name_lab || '';
|
||||
const status = results?.norm_status || AnalysisStatus.NORMAL;
|
||||
const value = results?.response_value || 0;
|
||||
const unit = results?.unit || '';
|
||||
const normLowerIncluded = results?.norm_lower_included || false;
|
||||
const normUpperIncluded = results?.norm_upper_included || false;
|
||||
const normLower = results?.norm_lower || 0;
|
||||
const normUpper = results?.norm_upper || 0;
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const analysisResultLevel = useMemo(() => {
|
||||
if (!results) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isUnderNorm = value < normLower;
|
||||
if (isUnderNorm) {
|
||||
switch (status) {
|
||||
case AnalysisStatus.MEDIUM:
|
||||
return AnalysisResultLevel.LOW;
|
||||
default:
|
||||
return AnalysisResultLevel.VERY_LOW;
|
||||
}
|
||||
}
|
||||
switch (status) {
|
||||
case AnalysisStatus.MEDIUM:
|
||||
return AnalysisResultLevel.HIGH;
|
||||
case AnalysisStatus.HIGH:
|
||||
return AnalysisResultLevel.VERY_HIGH;
|
||||
default:
|
||||
return AnalysisResultLevel.NORMAL;
|
||||
}
|
||||
}, [results, value, normLower]);
|
||||
|
||||
return (
|
||||
<div className="border-border rounded-lg border px-5">
|
||||
<div className="flex flex-col items-center justify-between gap-2 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
{startIcon || <div className="w-4" />}
|
||||
{name}
|
||||
{results?.response_time && (
|
||||
<div
|
||||
className="group/tooltip relative"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowTooltip(!showTooltip);
|
||||
}}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
<Info className="hover" />{' '}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute bottom-full left-1/2 z-10 mb-2 hidden -translate-x-1/2 rounded border bg-white p-4 text-sm whitespace-nowrap group-hover/tooltip:block',
|
||||
{ block: showTooltip },
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="analysis-results:analysisDate" />
|
||||
{': '}
|
||||
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{results ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<div className="font-semibold">{value}</div>
|
||||
<div className="text-muted-foreground text-sm">{unit}</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
||||
{normLower} - {normUpper}
|
||||
<div>
|
||||
<Trans i18nKey="analysis-results:results.range.normal" />
|
||||
</div>
|
||||
</div>
|
||||
<AnalysisLevelBar
|
||||
results={results}
|
||||
normLowerIncluded={normLowerIncluded}
|
||||
normUpperIncluded={normUpperIncluded}
|
||||
level={analysisResultLevel!}
|
||||
/>
|
||||
{endIcon || <div className="mx-2 w-4" />}
|
||||
</>
|
||||
) : isCancelled ? null : (
|
||||
<>
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<div className="font-semibold">
|
||||
<Trans i18nKey="analysis-results:waitingForResults" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-8 w-[60px]"></div>
|
||||
<AnalysisLevelBarSkeleton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisDoctor;
|
||||
@@ -1,27 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Spinner } from '@kit/ui/makerkit/spinner';
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
import { Progress } from '@kit/ui/shadcn/progress';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
const AnalysisFallback = ({
|
||||
progress,
|
||||
progressTextKey,
|
||||
}: {
|
||||
progress: number;
|
||||
progressTextKey: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-10">
|
||||
<Trans i18nKey={progressTextKey} />
|
||||
<Spinner />
|
||||
<Progress value={progress} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withI18n(AnalysisFallback);
|
||||
@@ -1,177 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { giveFeedbackAction } from '@/packages/features/doctor/src/lib/server/actions/doctor-server-actions';
|
||||
import {
|
||||
DoctorFeedback,
|
||||
Order,
|
||||
Patient,
|
||||
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
|
||||
import {
|
||||
DoctorAnalysisFeedbackForm,
|
||||
doctorAnalysisFeedbackFormSchema,
|
||||
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema';
|
||||
import ConfirmationModal from '@/packages/shared/src/components/confirmation-modal';
|
||||
import { useUser } from '@/packages/supabase/src/hooks/use-user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Spinner } from '@kit/ui/makerkit/spinner';
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@kit/ui/shadcn/form';
|
||||
import { toast } from '@kit/ui/shadcn/sonner';
|
||||
import { Textarea } from '@kit/ui/shadcn/textarea';
|
||||
|
||||
const AnalysisFeedback = ({
|
||||
feedback,
|
||||
patient,
|
||||
order,
|
||||
aiDoctorFeedback,
|
||||
timestamp,
|
||||
recommendations,
|
||||
isRecommendationsEdited,
|
||||
}: {
|
||||
feedback?: DoctorFeedback;
|
||||
patient: Patient;
|
||||
order: Order;
|
||||
aiDoctorFeedback?: string;
|
||||
timestamp?: string;
|
||||
recommendations: string[];
|
||||
isRecommendationsEdited: boolean;
|
||||
}) => {
|
||||
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
||||
const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false);
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const { data: user } = useUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(doctorAnalysisFeedbackFormSchema),
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
feedbackValue: feedback?.value ?? aiDoctorFeedback ?? '',
|
||||
userId: patient.userId,
|
||||
},
|
||||
});
|
||||
|
||||
const isReadOnly =
|
||||
!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id;
|
||||
|
||||
const handleDraftSubmit = async (e: React.FormEvent) => {
|
||||
setIsDraftSubmitting(true);
|
||||
e.preventDefault();
|
||||
|
||||
form.formState.errors.feedbackValue = undefined;
|
||||
const formData = form.getValues();
|
||||
await onSubmit(formData, 'DRAFT');
|
||||
setIsDraftSubmitting(false);
|
||||
};
|
||||
const handleCompleteSubmit = form.handleSubmit(async () => {
|
||||
setIsConfirmOpen(true);
|
||||
});
|
||||
|
||||
const onSubmit = async (
|
||||
data: DoctorAnalysisFeedbackForm,
|
||||
status: 'DRAFT' | 'COMPLETED',
|
||||
) => {
|
||||
setIsConfirmOpen(false);
|
||||
setIsSubmittingFeedback(true);
|
||||
|
||||
const result = await giveFeedbackAction({
|
||||
...data,
|
||||
analysisOrderId: order.analysisOrderId,
|
||||
status,
|
||||
patientId: patient.userId,
|
||||
timestamp,
|
||||
recommendations,
|
||||
isRecommendationsEdited,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return toast.error(<Trans i18nKey="common:genericServerError" />);
|
||||
}
|
||||
|
||||
setIsSubmittingFeedback(false);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => query.queryKey.includes('doctor-jobs'),
|
||||
});
|
||||
|
||||
return toast.success(<Trans i18nKey={'doctor:updateFeedbackSuccess'} />);
|
||||
};
|
||||
|
||||
const confirmComplete = form.handleSubmit(async (data) => {
|
||||
await onSubmit(data, 'COMPLETED');
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isReadOnly && (
|
||||
<Form {...form}>
|
||||
<form className="space-y-4 lg:w-1/2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="feedbackValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="min-h-[200px]"
|
||||
{...field}
|
||||
disabled={isDraftSubmitting || isSubmittingFeedback}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="xs:flex block justify-end gap-2 space-y-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleDraftSubmit}
|
||||
disabled={
|
||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||
}
|
||||
className="xs:w-auto w-full text-xs"
|
||||
>
|
||||
<Trans i18nKey="common:saveAsDraft" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCompleteSubmit}
|
||||
disabled={
|
||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||
}
|
||||
className="xs:w-1/4 w-full"
|
||||
>
|
||||
{isDraftSubmitting || form.formState.isSubmitting ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Trans i18nKey="common:save" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmOpen}
|
||||
onClose={() => setIsConfirmOpen(false)}
|
||||
onConfirm={confirmComplete}
|
||||
titleKey="doctor:confirmFeedbackModal.title"
|
||||
descriptionKey="doctor:confirmFeedbackModal.description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisFeedback;
|
||||
@@ -1,132 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { ArrowDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { AnalysisResultForDisplay } from './analysis-doctor';
|
||||
|
||||
export enum AnalysisResultLevel {
|
||||
VERY_LOW = 0,
|
||||
LOW = 1,
|
||||
NORMAL = 2,
|
||||
HIGH = 3,
|
||||
VERY_HIGH = 4,
|
||||
}
|
||||
|
||||
const Level = ({
|
||||
isActive = false,
|
||||
color,
|
||||
isFirst = false,
|
||||
isLast = false,
|
||||
arrowLocation,
|
||||
}: {
|
||||
isActive?: boolean;
|
||||
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
arrowLocation?: number;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(`bg-${color} relative h-3 flex-1`, {
|
||||
'opacity-20': !isActive,
|
||||
'rounded-l-lg': isFirst,
|
||||
'rounded-r-lg': isLast,
|
||||
})}
|
||||
>
|
||||
{isActive && (
|
||||
<div
|
||||
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
||||
style={{ left: `${arrowLocation}%` }}
|
||||
>
|
||||
<ArrowDown strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnalysisLevelBarSkeleton = () => {
|
||||
return (
|
||||
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||
<Level color="gray-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AnalysisLevelBar = ({
|
||||
normLowerIncluded = true,
|
||||
normUpperIncluded = true,
|
||||
level,
|
||||
results,
|
||||
}: {
|
||||
normLowerIncluded?: boolean;
|
||||
normUpperIncluded?: boolean;
|
||||
level: AnalysisResultLevel;
|
||||
results: AnalysisResultForDisplay;
|
||||
}) => {
|
||||
const {
|
||||
norm_lower: lower,
|
||||
norm_upper: upper,
|
||||
response_value: value,
|
||||
} = results;
|
||||
const arrowLocation = useMemo(() => {
|
||||
if (value < lower!) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (normLowerIncluded || normUpperIncluded) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
const calculated = ((value - lower!) / (upper! - lower!)) * 100;
|
||||
|
||||
if (calculated > 100) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return calculated;
|
||||
}, [value, upper, lower]);
|
||||
|
||||
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(
|
||||
() => [
|
||||
level === AnalysisResultLevel.VERY_LOW,
|
||||
level === AnalysisResultLevel.LOW,
|
||||
level === AnalysisResultLevel.HIGH,
|
||||
level === AnalysisResultLevel.VERY_HIGH,
|
||||
],
|
||||
[level, value, upper, lower],
|
||||
);
|
||||
|
||||
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||
{normLowerIncluded && (
|
||||
<>
|
||||
<Level isActive={isVeryLow} color="destructive" isFirst />
|
||||
<Level isActive={isLow} color="warning" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Level
|
||||
isFirst={!normLowerIncluded}
|
||||
isLast={!normUpperIncluded}
|
||||
{...(hasAbnormalLevel
|
||||
? { color: 'warning', isActive: false }
|
||||
: { color: 'success', isActive: true })}
|
||||
arrowLocation={arrowLocation}
|
||||
/>
|
||||
|
||||
{normUpperIncluded && (
|
||||
<>
|
||||
<Level isActive={isHigh} color="warning" />
|
||||
<Level isActive={isVeryHigh} color="destructive" isLast />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisLevelBar;
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { capitalize } from 'lodash';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
|
||||
import {
|
||||
getDOBWithAgeStringFromPersonalCode,
|
||||
getResultSetName,
|
||||
@@ -15,50 +18,48 @@ import {
|
||||
Order,
|
||||
Patient,
|
||||
} from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
|
||||
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
|
||||
import {
|
||||
DoctorAnalysisFeedbackForm,
|
||||
doctorAnalysisFeedbackFormSchema,
|
||||
} from '@kit/doctor/schema/doctor-analysis.schema';
|
||||
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
||||
import {
|
||||
useCurrentLocaleLanguageNames
|
||||
} from '@kit/shared/hooks';
|
||||
import { getFullName } from '@kit/shared/utils';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Textarea } from '@kit/ui/textarea';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { bmiFromMetric } from '~/lib/utils';
|
||||
|
||||
import AnalysisFeedback from './analysis-feedback';
|
||||
import DoctorAnalysisWrapper from './doctor-analysis-wrapper';
|
||||
import DoctorJobSelect from './doctor-job-select';
|
||||
import DoctorRecommendedAnalyses from './doctor-recommended-analyses';
|
||||
|
||||
export default function AnalysisView({
|
||||
patient,
|
||||
order,
|
||||
analyses,
|
||||
feedback,
|
||||
aiDoctorFeedback,
|
||||
recommendations,
|
||||
availableAnalyses,
|
||||
timestamp,
|
||||
}: {
|
||||
patient: Patient;
|
||||
order: Order;
|
||||
analyses: AnalysisResponse[];
|
||||
feedback?: DoctorFeedback;
|
||||
aiDoctorFeedback?: string;
|
||||
recommendations?: string[];
|
||||
availableAnalyses?: string[];
|
||||
timestamp?: string;
|
||||
}) {
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
||||
|
||||
const { data: user } = useUser();
|
||||
const queryClient = useQueryClient();
|
||||
const [recommendedAnalyses, setRecommendedAnalyses] = useState<string[]>(
|
||||
recommendations ?? [],
|
||||
);
|
||||
const isRecommendationsEdited = useMemo(() => {
|
||||
if (recommendedAnalyses.length !== recommendations?.length) return true;
|
||||
const sa = new Set(recommendedAnalyses),
|
||||
sb = new Set(recommendations);
|
||||
if (sa.size !== sb.size) return true;
|
||||
for (const v of sa) if (!sb.has(v)) return true;
|
||||
return false;
|
||||
}, [recommendations, recommendedAnalyses]);
|
||||
|
||||
const languageNames = useCurrentLocaleLanguageNames();
|
||||
|
||||
@@ -69,11 +70,66 @@ export default function AnalysisView({
|
||||
);
|
||||
const isCurrentDoctorJob =
|
||||
!!feedback?.doctor_user_id && feedback?.doctor_user_id === user?.id;
|
||||
const isReadOnly =
|
||||
!isInProgress ||
|
||||
(!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(doctorAnalysisFeedbackFormSchema),
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
feedbackValue: feedback?.value ?? '',
|
||||
userId: patient.userId,
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
if (!patient || !order || !analyses) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmit = async (
|
||||
data: DoctorAnalysisFeedbackForm,
|
||||
status: 'DRAFT' | 'COMPLETED',
|
||||
) => {
|
||||
const result = await giveFeedbackAction({
|
||||
...data,
|
||||
analysisOrderId: order.analysisOrderId,
|
||||
status,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return toast.error(<Trans i18nKey="common:genericServerError" />);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => query.queryKey.includes('doctor-jobs'),
|
||||
});
|
||||
|
||||
toast.success(<Trans i18nKey={'doctor:updateFeedbackSuccess'} />);
|
||||
|
||||
return setIsConfirmOpen(false);
|
||||
};
|
||||
|
||||
const handleDraftSubmit = async (e: React.FormEvent) => {
|
||||
setIsDraftSubmitting(true);
|
||||
e.preventDefault();
|
||||
|
||||
form.formState.errors.feedbackValue = undefined;
|
||||
const formData = form.getValues();
|
||||
await onSubmit(formData, 'DRAFT');
|
||||
setIsDraftSubmitting(false);
|
||||
};
|
||||
|
||||
const handleCompleteSubmit = form.handleSubmit(async () => {
|
||||
setIsConfirmOpen(true);
|
||||
});
|
||||
|
||||
const confirmComplete = form.handleSubmit(async (data) => {
|
||||
await onSubmit(data, 'COMPLETED');
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="xs:flex xs:justify-between">
|
||||
@@ -175,30 +231,59 @@ export default function AnalysisView({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{order.isPackage && (
|
||||
<>
|
||||
<h3>
|
||||
<Trans i18nKey="doctor:feedback" />
|
||||
</h3>
|
||||
<p>{feedback?.value ?? '-'}</p>
|
||||
<div className="flex flex-col gap-4 lg:flex-row">
|
||||
<AnalysisFeedback
|
||||
order={order}
|
||||
patient={patient}
|
||||
feedback={feedback}
|
||||
aiDoctorFeedback={aiDoctorFeedback}
|
||||
timestamp={timestamp}
|
||||
recommendations={recommendedAnalyses}
|
||||
isRecommendationsEdited={isRecommendationsEdited}
|
||||
<h3>
|
||||
<Trans i18nKey="doctor:feedback" />
|
||||
</h3>
|
||||
<p>{feedback?.value ?? '-'}</p>
|
||||
{!isReadOnly && (
|
||||
<Form {...form}>
|
||||
<form className="space-y-4 lg:w-1/2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="feedbackValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea {...field} disabled={isReadOnly} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DoctorRecommendedAnalyses
|
||||
recommendedAnalyses={recommendedAnalyses}
|
||||
availableAnalyses={availableAnalyses}
|
||||
setRecommendedAnalyses={setRecommendedAnalyses}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div className="xs:flex block justify-end gap-2 space-y-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleDraftSubmit}
|
||||
disabled={
|
||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||
}
|
||||
className="xs:w-1/4 w-full"
|
||||
>
|
||||
<Trans i18nKey="common:saveAsDraft" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCompleteSubmit}
|
||||
disabled={
|
||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||
}
|
||||
className="xs:w-1/4 w-full"
|
||||
>
|
||||
<Trans i18nKey="common:save" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmOpen}
|
||||
onClose={() => setIsConfirmOpen(false)}
|
||||
onConfirm={confirmComplete}
|
||||
titleKey="doctor:confirmFeedbackModal.title"
|
||||
descriptionKey="doctor:confirmFeedbackModal.description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@kit/ui/collapsible';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import AnalysisDoctor from './analysis-doctor';
|
||||
import Analysis from '~/home/(user)/(dashboard)/analysis-results/_components/analysis';
|
||||
|
||||
export default function DoctorAnalysisWrapper({
|
||||
analysisData,
|
||||
@@ -29,7 +29,7 @@ export default function DoctorAnalysisWrapper({
|
||||
asChild
|
||||
>
|
||||
<div className="[&[data-state=open]_.caret-icon]:rotate-180">
|
||||
<AnalysisDoctor
|
||||
<Analysis
|
||||
startIcon={
|
||||
analysisData.latestPreviousAnalysis && (
|
||||
<CaretDownIcon className="caret-icon transition-transform duration-200" />
|
||||
@@ -65,7 +65,7 @@ export default function DoctorAnalysisWrapper({
|
||||
{analysisData.latestPreviousAnalysis && (
|
||||
<CollapsibleContent>
|
||||
<div className="my-1 flex flex-col">
|
||||
<AnalysisDoctor
|
||||
<Analysis
|
||||
endIcon={
|
||||
analysisData.latestPreviousAnalysis.comment && (
|
||||
<>
|
||||
|
||||
@@ -6,10 +6,9 @@ import {
|
||||
getUserDoneResponsesAction,
|
||||
getUserInProgressResponsesAction,
|
||||
} from '@kit/doctor/actions/table-data-fetching-actions';
|
||||
|
||||
import ResultsTableWrapper from './results-table-wrapper';
|
||||
|
||||
export default function DoctorDashboard() {
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<ResultsTableWrapper
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { isDoctor } from '@kit/doctor/lib/server/utils/is-doctor';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { isDoctor } from '@kit/doctor/lib/server/utils/is-doctor';
|
||||
|
||||
|
||||
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
|
||||
const DoctorRecommendedAnalyses = ({
|
||||
recommendedAnalyses,
|
||||
availableAnalyses,
|
||||
setRecommendedAnalyses,
|
||||
}: {
|
||||
recommendedAnalyses?: string[];
|
||||
availableAnalyses?: string[];
|
||||
setRecommendedAnalyses: Dispatch<SetStateAction<string[]>>;
|
||||
}) => {
|
||||
if (availableAnalyses?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-1/3">
|
||||
<h5>
|
||||
<Trans i18nKey="doctor:recommendedAnalyses" />
|
||||
</h5>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{availableAnalyses?.map((analysis, index) => {
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
key={`${index}-analysis-feedback-list`}
|
||||
variant={
|
||||
recommendedAnalyses?.includes(analysis) ? 'default' : 'outline'
|
||||
}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setRecommendedAnalyses((prev: string[]) =>
|
||||
prev.includes(analysis)
|
||||
? prev.filter((x) => x !== analysis)
|
||||
: [...prev, analysis],
|
||||
)
|
||||
}
|
||||
>
|
||||
{analysis}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoctorRecommendedAnalyses;
|
||||
@@ -34,7 +34,7 @@ export function DoctorSidebar({
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className={'m-2'}>
|
||||
<AppLogo
|
||||
href={pathsConfig.app.home}
|
||||
href={pathsConfig.app.doctor}
|
||||
className="max-w-full"
|
||||
compact={!open}
|
||||
/>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AnalysisResponses } from '@/app/home/(user)/_components/ai/types';
|
||||
import { OrderAnalysisCard } from '@/app/home/(user)/_components/order-analyses-cards';
|
||||
import { loadLifeStyle } from '@/app/home/(user)/_lib/server/load-life-style';
|
||||
import { loadRecommendations } from '@/app/home/(user)/_lib/server/load-recommendations';
|
||||
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
||||
import { AnalysisResultDetails } from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import {
|
||||
loadDoctorFeedback,
|
||||
prepareFeedback,
|
||||
} from '../_lib/server/load-doctor-feedback';
|
||||
import AnalysisView from './analysis-view';
|
||||
|
||||
async function NewAnalysisRecommendationsLoader({
|
||||
analysisResultDetails,
|
||||
account,
|
||||
analysisResponses,
|
||||
currentAIResponseTimestamp,
|
||||
analyses,
|
||||
patient,
|
||||
}: {
|
||||
currentAIResponseTimestamp: string;
|
||||
account: AccountWithParams | null;
|
||||
analysisResponses: AnalysisResponses;
|
||||
analysisResultDetails: AnalysisResultDetails;
|
||||
analyses: OrderAnalysisCard[];
|
||||
patient: AccountWithParams | null;
|
||||
}) {
|
||||
if (!analysisResultDetails.order.isPackage) {
|
||||
return (
|
||||
<AnalysisView
|
||||
patient={analysisResultDetails.patient}
|
||||
order={analysisResultDetails.order}
|
||||
analyses={analysisResultDetails.analysisResponse}
|
||||
feedback={analysisResultDetails.doctorFeedback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const [lifeStyle, recommendations, aiFeedback] = await Promise.all([
|
||||
loadLifeStyle({
|
||||
account: patient,
|
||||
analysisResponses,
|
||||
isDoctorView: true,
|
||||
aiResponseTimestamp: currentAIResponseTimestamp,
|
||||
}),
|
||||
loadRecommendations({
|
||||
account: patient,
|
||||
analysisResponses,
|
||||
aiResponseTimestamp: currentAIResponseTimestamp,
|
||||
isDoctorView: true,
|
||||
analyses,
|
||||
}),
|
||||
loadDoctorFeedback(
|
||||
analysisResultDetails.patient,
|
||||
analysisResultDetails.analysisResponse,
|
||||
currentAIResponseTimestamp,
|
||||
),
|
||||
]);
|
||||
|
||||
const feedback = prepareFeedback({
|
||||
aiResponse: aiFeedback,
|
||||
recommendations,
|
||||
lifeStyleSummary: lifeStyle.response.summary,
|
||||
patientName: analysisResultDetails.patient.firstName,
|
||||
doctorName: `${account?.name} ${account?.last_name}`,
|
||||
aiResponseTimestamp: currentAIResponseTimestamp,
|
||||
});
|
||||
|
||||
return (
|
||||
<AnalysisView
|
||||
patient={analysisResultDetails.patient}
|
||||
order={analysisResultDetails.order}
|
||||
analyses={analysisResultDetails.analysisResponse}
|
||||
feedback={analysisResultDetails.doctorFeedback}
|
||||
aiDoctorFeedback={feedback}
|
||||
availableAnalyses={analyses.map((analysis) => analysis.title)}
|
||||
recommendations={recommendations}
|
||||
timestamp={currentAIResponseTimestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(NewAnalysisRecommendationsLoader);
|
||||
@@ -1,55 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
import { loadAnalyses } from '@/app/home/(user)/_lib/server/load-analyses';
|
||||
import {
|
||||
loadCurrentUserAccount,
|
||||
loadUserAccount,
|
||||
} from '@/app/home/(user)/_lib/server/load-user-account';
|
||||
import { AnalysisResultDetails } from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
|
||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { getLatestResponseTime } from '~/lib/utils';
|
||||
|
||||
import AnalysisFallback from './analysis-fallback';
|
||||
import NewAnalysisRecommendationsLoader from './new-analysis-recommendations-loader';
|
||||
|
||||
async function PrepareAIParameters({
|
||||
analysisResultDetails,
|
||||
}: {
|
||||
analysisResultDetails: AnalysisResultDetails;
|
||||
}) {
|
||||
const { analyses } = await loadAnalyses();
|
||||
const { account: doctorAccount } = await loadCurrentUserAccount();
|
||||
const patientAccount = await loadUserAccount(
|
||||
analysisResultDetails.patient.userId,
|
||||
);
|
||||
const client = getSupabaseServerClient();
|
||||
const userAnalysesApi = createUserAnalysesApi(client);
|
||||
const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(
|
||||
patientAccount.id,
|
||||
);
|
||||
const currentAIResponseTimestamp = getLatestResponseTime(analysisResponses);
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<AnalysisFallback progress={66} progressTextKey="doctor:loadFeedback" />
|
||||
}
|
||||
>
|
||||
<NewAnalysisRecommendationsLoader
|
||||
account={doctorAccount}
|
||||
currentAIResponseTimestamp={currentAIResponseTimestamp}
|
||||
analysisResponses={analysisResponses}
|
||||
analysisResultDetails={analysisResultDetails}
|
||||
analyses={analyses}
|
||||
patient={patientAccount}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(PrepareAIParameters);
|
||||
@@ -5,7 +5,6 @@ import { useTransition } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { format } from 'date-fns';
|
||||
import { capitalize } from 'lodash';
|
||||
import { Eye } from 'lucide-react';
|
||||
|
||||
import { getResultSetName } from '@kit/doctor/lib/helpers';
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import DoctorJobSelect from './doctor-job-select';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
export default function ResultsTable({
|
||||
results = [],
|
||||
@@ -189,9 +189,7 @@ export default function ResultsTable({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{capitalize(
|
||||
languageNames.of(
|
||||
result?.patient?.preferred_locale ?? 'et',
|
||||
),
|
||||
languageNames.of(result?.patient?.preferred_locale ?? 'et'),
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { PROMPT_NAME } from '@/app/home/(user)/_components/ai/types';
|
||||
import { generateDoctorFeedback } from '@/app/home/(user)/_lib/server/ai-actions';
|
||||
import {
|
||||
AnalysisResponse,
|
||||
Patient,
|
||||
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
|
||||
import { getLogger } from '@/packages/shared/src/logger';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
export const loadDoctorFeedback = cache(doctorFeedbackLoader);
|
||||
|
||||
const PLACEHOLDER = {
|
||||
ANALYSES: 'SOOVITATUD_ANALYYSID_PLACEHOLDER',
|
||||
LIFE_STYLE_SUMMARY: 'ELUSTIILI_KOKKUVOTTE_PLACEHOLDER',
|
||||
PATIENT_NAME: 'PATSIENDI_NIMI_PLACEHOLDER',
|
||||
DOCTOR_NAME: 'ARSTI_NIMI_PLACEHOLDER',
|
||||
ANALYSES_DATE: 'ANALYYSI_KUUPAEV_PLACEHOLDER',
|
||||
};
|
||||
|
||||
export const prepareFeedback = ({
|
||||
aiResponse,
|
||||
recommendations,
|
||||
lifeStyleSummary,
|
||||
patientName,
|
||||
doctorName,
|
||||
aiResponseTimestamp,
|
||||
}: {
|
||||
aiResponse: string;
|
||||
recommendations?: string[];
|
||||
lifeStyleSummary: string | null;
|
||||
patientName: string;
|
||||
doctorName: string;
|
||||
aiResponseTimestamp: string;
|
||||
}) => {
|
||||
const recommendationsList = recommendations
|
||||
? recommendations.map((analysis) => `${analysis}`).join('\n')
|
||||
: '';
|
||||
const feedback = aiResponse
|
||||
.replace(PLACEHOLDER.ANALYSES, recommendationsList)
|
||||
.replace(PLACEHOLDER.LIFE_STYLE_SUMMARY, lifeStyleSummary ?? '')
|
||||
.replace(PLACEHOLDER.PATIENT_NAME, patientName)
|
||||
.replace(PLACEHOLDER.DOCTOR_NAME, doctorName)
|
||||
.replace(
|
||||
PLACEHOLDER.ANALYSES_DATE,
|
||||
new Date(aiResponseTimestamp).toLocaleString(),
|
||||
);
|
||||
|
||||
return feedback;
|
||||
};
|
||||
|
||||
async function doctorFeedbackLoader(
|
||||
patient: Patient | null,
|
||||
analysisResponses: AnalysisResponse[],
|
||||
aiResponseTimestamp: string,
|
||||
): Promise<string> {
|
||||
const logger = await getLogger();
|
||||
if (!patient?.personalCode) {
|
||||
return '';
|
||||
}
|
||||
const supabaseClient = getSupabaseServerClient();
|
||||
|
||||
logger.info(
|
||||
{
|
||||
aiResponseTimestamp,
|
||||
patientId: patient.userId,
|
||||
},
|
||||
'Attempting to receive existing doctor feedback',
|
||||
);
|
||||
|
||||
const { data, error } = await supabaseClient
|
||||
.schema('medreport')
|
||||
.from('ai_responses')
|
||||
.select('*')
|
||||
.eq('account_id', patient.userId)
|
||||
.eq('prompt_name', PROMPT_NAME.FEEDBACK)
|
||||
.eq('latest_data_change', aiResponseTimestamp)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
logger.info({ data: !!data }, 'Existing doctor feedback');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching AI response from DB: ', error);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (data?.response) {
|
||||
return data.response as string;
|
||||
} else {
|
||||
return await generateDoctorFeedback({
|
||||
patient,
|
||||
analysisResponses,
|
||||
aiResponseTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Suspense, cache } from 'react';
|
||||
import { cache } from 'react';
|
||||
|
||||
import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import {
|
||||
DoctorPageViewAction,
|
||||
createDoctorPageViewLog,
|
||||
} from '~/lib/services/audit/doctorPageView.service';
|
||||
|
||||
import AnalysisFallback from '../../_components/analysis-fallback';
|
||||
import AnalysisView from '../../_components/analysis-view';
|
||||
import { DoctorGuard } from '../../_components/doctor-guard';
|
||||
import PrepareAiParameters from '../../_components/prepare-ai-parameters';
|
||||
|
||||
async function AnalysisPage({
|
||||
params,
|
||||
@@ -38,21 +36,19 @@ async function AnalysisPage({
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
<PageBody className="px-12">
|
||||
<Suspense
|
||||
fallback={
|
||||
<AnalysisFallback
|
||||
progress={33}
|
||||
progressTextKey="doctor:loadParameters"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PrepareAiParameters analysisResultDetails={analysisResultDetails} />
|
||||
</Suspense>
|
||||
<PageBody>
|
||||
<AnalysisView
|
||||
patient={analysisResultDetails.patient}
|
||||
order={analysisResultDetails.order}
|
||||
analyses={analysisResultDetails.analysisResponse}
|
||||
feedback={analysisResultDetails.doctorFeedback}
|
||||
/>
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DoctorGuard(withI18n(AnalysisPage));
|
||||
export default DoctorGuard(AnalysisPage);
|
||||
const loadResult = cache(getAnalysisResultsForDoctor);
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ async function CompletedJobsPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
<PageBody className="px-12">
|
||||
<PageBody>
|
||||
<ResultsTableWrapper
|
||||
titleKey="doctor:completedReviews"
|
||||
action={getUserDoneResponsesAction}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cookies } from 'next/headers';
|
||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
|
||||
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
|
||||
import { DoctorSidebar } from './_components/doctor-sidebar';
|
||||
import { DoctorMobileNavigation } from './_components/mobile-navigation';
|
||||
|
||||
@@ -17,7 +17,7 @@ async function MyReviewsPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
<PageBody className="px-12">
|
||||
<PageBody>
|
||||
<ResultsTableWrapper
|
||||
titleKey="doctor:myReviews"
|
||||
action={getUserInProgressResponsesAction}
|
||||
|
||||
@@ -17,7 +17,7 @@ async function OpenJobsPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
<PageBody className="px-12">
|
||||
<PageBody>
|
||||
<ResultsTableWrapper
|
||||
titleKey="doctor:openReviews"
|
||||
action={getOpenResponsesAction}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
createDoctorPageViewLog,
|
||||
} from '~/lib/services/audit/doctorPageView.service';
|
||||
|
||||
import DoctorDashboard from './_components/doctor-dashboard';
|
||||
import Dashboard from './_components/doctor-dashboard';
|
||||
import { DoctorGuard } from './_components/doctor-guard';
|
||||
|
||||
async function DoctorPage() {
|
||||
@@ -16,8 +16,8 @@ async function DoctorPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
<PageBody className="px-12">
|
||||
<DoctorDashboard />
|
||||
<PageBody>
|
||||
<Dashboard />
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft, MessageCircle } from 'lucide-react';
|
||||
@@ -21,22 +20,6 @@ const ErrorPage = ({
|
||||
}) => {
|
||||
useCaptureException(error);
|
||||
|
||||
// Ignore next.js internal transient navigation errors that occur during auth state changes
|
||||
const isTransientNavigationError =
|
||||
error?.message?.includes('Error in input stream') ||
|
||||
error?.message?.includes('AbortError') ||
|
||||
(error?.name === 'ChunkLoadError');
|
||||
|
||||
useEffect(() => {
|
||||
if (isTransientNavigationError && typeof window !== 'undefined') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, [isTransientNavigationError]);
|
||||
|
||||
if (isTransientNavigationError) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex h-screen flex-1 flex-col'}>
|
||||
<SiteHeader />
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft, MessageCircle } from 'lucide-react';
|
||||
|
||||
import { useCaptureException } from '@kit/monitoring/hooks';
|
||||
import { RootProviders } from '@kit/shared/components/root-providers';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||
import { RootProviders } from '@kit/shared/components/root-providers';
|
||||
|
||||
const GlobalErrorPage = ({
|
||||
error,
|
||||
@@ -21,22 +20,6 @@ const GlobalErrorPage = ({
|
||||
reset: () => void;
|
||||
}) => {
|
||||
useCaptureException(error);
|
||||
|
||||
// Ignore next.js internal transient navigation errors that occur during auth state changes
|
||||
const isTransientNavigationError =
|
||||
error?.message?.includes('Error in input stream') ||
|
||||
error?.message?.includes('AbortError') ||
|
||||
(error?.name === 'ChunkLoadError');
|
||||
|
||||
useEffect(() => {
|
||||
if (isTransientNavigationError && typeof window !== 'undefined') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, [isTransientNavigationError]);
|
||||
|
||||
if (isTransientNavigationError) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<html>
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { formatDateAndTime } from '@kit/shared/utils';
|
||||
|
||||
import { loadCurrentUserAccount } from '~/home/(user)/_lib/server/load-user-account';
|
||||
import { loadUserAnalysis } from '~/home/(user)/_lib/server/load-user-analysis';
|
||||
@@ -29,17 +22,14 @@ export default async function AnalysisResultsPage({
|
||||
id: string;
|
||||
}>;
|
||||
}) {
|
||||
const supabaseClient = getSupabaseServerClient();
|
||||
const { id: analysisOrderId } = await params;
|
||||
const notificationsApi = createNotificationsApi(supabaseClient);
|
||||
const account = await loadCurrentUserAccount();
|
||||
|
||||
const [{ account }, analysisResponse] = await Promise.all([
|
||||
loadCurrentUserAccount(),
|
||||
loadUserAnalysis(Number(analysisOrderId)),
|
||||
]);
|
||||
const { id: analysisResponseId } = await params;
|
||||
|
||||
if (!account?.id) {
|
||||
return redirect('/');
|
||||
const analysisResponse = await loadUserAnalysis(Number(analysisResponseId));
|
||||
|
||||
if (!account?.id || !analysisResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await createPageViewLog({
|
||||
@@ -47,74 +37,46 @@ export default async function AnalysisResultsPage({
|
||||
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||
});
|
||||
|
||||
await notificationsApi.dismissNotification(
|
||||
`/home/analysis-results/${analysisOrderId}`,
|
||||
'link',
|
||||
);
|
||||
|
||||
if (!analysisResponse) {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||
description={<Trans i18nKey="analysis-results:descriptionEmpty" />}
|
||||
/>
|
||||
<PageBody className="gap-4"></PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
|
||||
const hasOrderedAnalysisElements = orderedAnalysisElements.length > 0;
|
||||
const isPartialStatus =
|
||||
analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||
description={
|
||||
hasOrderedAnalysisElements ? (
|
||||
isPartialStatus ? (
|
||||
<Trans i18nKey="analysis-results:descriptionPartial" />
|
||||
) : (
|
||||
<Trans i18nKey="analysis-results:description" />
|
||||
)
|
||||
) : (
|
||||
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<PageHeader />
|
||||
<PageBody className="gap-4">
|
||||
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
|
||||
<div>
|
||||
<h4>
|
||||
<Trans i18nKey="analysis-results:pageTitle" />
|
||||
</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{analysisResponse?.elements &&
|
||||
analysisResponse.elements?.length > 0 ? (
|
||||
<Trans i18nKey="analysis-results:description" />
|
||||
) : (
|
||||
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href={pathsConfig.app.orderAnalysisPackage}>
|
||||
<Trans i18nKey="analysis-results:orderNewAnalysis" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageBody className="gap-4 pt-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h5 className="break-all">
|
||||
<h4>
|
||||
<Trans
|
||||
i18nKey="analysis-results:orderTitle"
|
||||
values={{ orderNumber: analysisResponse.order.medusaOrderId }}
|
||||
values={{ orderNumber: analysisResponse.order.medusa_order_id }}
|
||||
/>
|
||||
</h4>
|
||||
<h5>
|
||||
<Trans
|
||||
i18nKey={`orders:status.${analysisResponse.order.status}`}
|
||||
/>
|
||||
</h5>
|
||||
<h6>
|
||||
<Trans i18nKey={`orders:status.${analysisResponse.order.status}`} />
|
||||
<ButtonTooltip
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="analysis-results:orderCreatedAt"
|
||||
values={{
|
||||
createdAt: formatDateAndTime(analysisResponse.order.createdAt)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
content={`${analysisResponse.order.created_at ? new Date(analysisResponse?.order?.created_at).toLocaleString() : ''}`}
|
||||
className="ml-6"
|
||||
/>
|
||||
</h6>
|
||||
</h5>
|
||||
</div>
|
||||
{analysisResponse?.summary?.value && (
|
||||
<div>
|
||||
@@ -125,20 +87,13 @@ export default async function AnalysisResultsPage({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{orderedAnalysisElements ? (
|
||||
orderedAnalysisElements.map((element) => (
|
||||
<React.Fragment key={element.analysisIdOriginal}>
|
||||
<Analysis element={element} />
|
||||
{element.results?.nestedElements?.map(
|
||||
(nestedElement, nestedIndex) => (
|
||||
<Analysis
|
||||
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
||||
nestedElement={nestedElement}
|
||||
isNestedElement
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</React.Fragment>
|
||||
{analysisResponse.elements ? (
|
||||
analysisResponse.elements.map((element, index) => (
|
||||
<Analysis
|
||||
key={index}
|
||||
analysisElement={{ analysis_name_lab: element.analysis_name }}
|
||||
results={element}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { AnalysisResultDetailsElementResults } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { ArrowDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
import { AnalysisResultForDisplay } from './analysis';
|
||||
|
||||
type AnalysisResultLevelBarResults = Pick<
|
||||
AnalysisResultDetailsElementResults,
|
||||
'normLower' | 'normUpper' | 'responseValue'
|
||||
>;
|
||||
export enum AnalysisResultLevel {
|
||||
VERY_LOW = 0,
|
||||
LOW = 1,
|
||||
NORMAL = 2,
|
||||
HIGH = 3,
|
||||
VERY_HIGH = 4,
|
||||
}
|
||||
|
||||
const Level = ({
|
||||
isActive = false,
|
||||
@@ -17,19 +19,17 @@ const Level = ({
|
||||
isFirst = false,
|
||||
isLast = false,
|
||||
arrowLocation,
|
||||
normRangeText,
|
||||
}: {
|
||||
isActive?: boolean;
|
||||
color: 'destructive' | 'success' | 'warning' | 'gray-200';
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
arrowLocation?: number;
|
||||
normRangeText?: string | null;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(`bg-${color} relative h-3 flex-1`, {
|
||||
'opacity-60': !isActive,
|
||||
'opacity-20': !isActive,
|
||||
'rounded-l-lg': isFirst,
|
||||
'rounded-r-lg': isLast,
|
||||
})}
|
||||
@@ -37,171 +37,96 @@ const Level = ({
|
||||
{isActive && (
|
||||
<div
|
||||
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
||||
{...(arrowLocation
|
||||
? {
|
||||
style: {
|
||||
left: `${arrowLocation}%`,
|
||||
...(arrowLocation > 92.5 && { left: '92.5%' }),
|
||||
...(arrowLocation < 7.5 && { left: '7.5%' }),
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
style={{ left: `${arrowLocation}%` }}
|
||||
>
|
||||
<ArrowDown strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{color === 'success' && typeof normRangeText === 'string' && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground absolute bottom-[-18px] left-3/8 text-xs font-bold whitespace-nowrap',
|
||||
{
|
||||
'opacity-60': isActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{normRangeText}
|
||||
</p>
|
||||
)}
|
||||
export const AnalysisLevelBarSkeleton = () => {
|
||||
return (
|
||||
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
||||
<Level color="gray-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AnalysisLevelBar = ({
|
||||
normLowerIncluded = true,
|
||||
normUpperIncluded = true,
|
||||
level,
|
||||
results: { normLower: lower, normUpper: upper, responseValue: value },
|
||||
normRangeText,
|
||||
results,
|
||||
}: {
|
||||
normLowerIncluded?: boolean;
|
||||
normUpperIncluded?: boolean;
|
||||
level: AnalysisResultLevel;
|
||||
results: AnalysisResultLevelBarResults;
|
||||
normRangeText: string | null;
|
||||
results: AnalysisResultForDisplay;
|
||||
}) => {
|
||||
// Calculate arrow position based on value within normal range
|
||||
|
||||
const { norm_lower: lower, norm_upper: upper, response_value: value } = results;
|
||||
const arrowLocation = useMemo(() => {
|
||||
// If no response value, center the arrow
|
||||
if (value === null || value === undefined) {
|
||||
if (value < lower!) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (normLowerIncluded || normUpperIncluded) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
// If no normal ranges defined, center the arrow
|
||||
if (lower === null && upper === null) {
|
||||
return 50;
|
||||
const calculated = ((value - lower!) / (upper! - lower!)) * 100;
|
||||
|
||||
if (calculated > 100) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// If only upper bound exists
|
||||
if (lower === null && upper !== null) {
|
||||
if (value <= upper) {
|
||||
return Math.min(75, (value / upper) * 75); // Show in left 75% of normal range
|
||||
}
|
||||
return 100; // Beyond upper bound
|
||||
}
|
||||
|
||||
// If only lower bound exists
|
||||
if (upper === null && lower !== null) {
|
||||
if (value >= lower) {
|
||||
// Value is in normal range (above lower bound)
|
||||
// Position proportionally in the normal range section
|
||||
const normalizedPosition = Math.min((value - lower) / (lower * 0.5), 1); // Use 50% of lower as scale
|
||||
return normalizedPosition * 100;
|
||||
}
|
||||
// Value is below lower bound - position in the "below normal" section
|
||||
const belowPosition = Math.max(0, Math.min(1, value / lower));
|
||||
return belowPosition * 100;
|
||||
}
|
||||
|
||||
// Both bounds exist
|
||||
if (lower !== null && upper !== null) {
|
||||
if (value < lower) {
|
||||
return 0; // Below normal range
|
||||
}
|
||||
if (value > upper) {
|
||||
return 100; // Above normal range
|
||||
}
|
||||
// Within normal range
|
||||
return ((value - lower) / (upper - lower)) * 100;
|
||||
}
|
||||
|
||||
return 50; // Fallback
|
||||
|
||||
return calculated;
|
||||
}, [value, upper, lower]);
|
||||
|
||||
// Determine level states based on normStatus
|
||||
const isNormal = level === AnalysisResultLevel.NORMAL;
|
||||
const isWarning = level === AnalysisResultLevel.WARNING;
|
||||
const isCritical = level === AnalysisResultLevel.CRITICAL;
|
||||
const isPending = level === null;
|
||||
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [
|
||||
level === AnalysisResultLevel.VERY_LOW,
|
||||
level === AnalysisResultLevel.LOW,
|
||||
level === AnalysisResultLevel.HIGH,
|
||||
level === AnalysisResultLevel.VERY_HIGH,
|
||||
], [level, value, upper, lower]);
|
||||
|
||||
// If pending results, show gray bar
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="w-60% mt-4 flex h-3 max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||
<Level color="gray-200" isFirst isLast />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show appropriate levels based on available norm bounds
|
||||
const hasLowerBound = lower !== null;
|
||||
const hasUpperBound = upper !== null;
|
||||
|
||||
// Determine which section the value falls into
|
||||
const isValueBelowLower = hasLowerBound && value !== null && value < lower!;
|
||||
const isValueAboveUpper = hasUpperBound && value !== null && value > upper!;
|
||||
const isValueInNormalRange = !isValueBelowLower && !isValueAboveUpper;
|
||||
|
||||
const [first, second, third] = useMemo(() => {
|
||||
const [warning, normal, critical] = [
|
||||
{
|
||||
isActive: isWarning,
|
||||
color: 'warning',
|
||||
...(isWarning ? { arrowLocation } : {}),
|
||||
},
|
||||
{
|
||||
isActive: isNormal,
|
||||
color: 'success',
|
||||
normRangeText,
|
||||
...(isNormal ? { arrowLocation } : {}),
|
||||
},
|
||||
{
|
||||
isActive: isCritical,
|
||||
color: 'destructive',
|
||||
isLast: true,
|
||||
...(isCritical ? { arrowLocation } : {}),
|
||||
},
|
||||
] as const;
|
||||
|
||||
if (!hasLowerBound) {
|
||||
return [{ ...normal, isFirst: true }, warning, critical] as const;
|
||||
}
|
||||
|
||||
return [
|
||||
{ ...warning, isFirst: true },
|
||||
normal,
|
||||
{ ...critical, isLast: true },
|
||||
] as const;
|
||||
}, [
|
||||
isValueBelowLower,
|
||||
isValueAboveUpper,
|
||||
isValueInNormalRange,
|
||||
arrowLocation,
|
||||
normRangeText,
|
||||
isNormal,
|
||||
isWarning,
|
||||
isCritical,
|
||||
]);
|
||||
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-3 gap-1',
|
||||
'mt-4 sm:mt-0',
|
||||
'w-[60%] sm:w-[35%]',
|
||||
'min-w-[50vw] sm:min-w-auto',
|
||||
'max-w-[360px]',
|
||||
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
||||
{normLowerIncluded && (
|
||||
<>
|
||||
<Level
|
||||
isActive={isVeryLow}
|
||||
color="destructive"
|
||||
isFirst
|
||||
/>
|
||||
<Level isActive={isLow} color="warning" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Level
|
||||
isFirst={!normLowerIncluded}
|
||||
isLast={!normUpperIncluded}
|
||||
{...(hasAbnormalLevel ? { color: "warning", isActive: false } : { color: "success", isActive: true })}
|
||||
arrowLocation={arrowLocation}
|
||||
/>
|
||||
|
||||
{normUpperIncluded && (
|
||||
<>
|
||||
<Level
|
||||
isActive={isHigh}
|
||||
color="warning"
|
||||
/>
|
||||
<Level
|
||||
isActive={isVeryHigh}
|
||||
color="destructive"
|
||||
isLast
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<Level {...first} />
|
||||
<Level {...second} />
|
||||
<Level {...third} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,142 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { ReactElement, ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
import type {
|
||||
AnalysisResultDetailsElement,
|
||||
AnalysisResultsDetailsElementNested,
|
||||
} from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
|
||||
import { format } from 'date-fns';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import AnalysisLevelBar from './analysis-level-bar';
|
||||
import { AnalysisElement } from '~/lib/services/analysis-element.service';
|
||||
|
||||
import AnalysisLevelBar, {
|
||||
AnalysisLevelBarSkeleton,
|
||||
AnalysisResultLevel,
|
||||
} from './analysis-level-bar';
|
||||
|
||||
export type AnalysisResultForDisplay = Pick<
|
||||
UserAnalysisElement,
|
||||
| 'norm_status'
|
||||
| 'response_value'
|
||||
| 'unit'
|
||||
| 'norm_lower_included'
|
||||
| 'norm_upper_included'
|
||||
| 'norm_lower'
|
||||
| 'norm_upper'
|
||||
| 'response_time'
|
||||
>;
|
||||
|
||||
export enum AnalysisStatus {
|
||||
NORMAL = 0,
|
||||
MEDIUM = 1,
|
||||
HIGH = 2,
|
||||
}
|
||||
|
||||
const Analysis = ({
|
||||
element: elementOriginal,
|
||||
nestedElement,
|
||||
isNestedElement = false,
|
||||
analysisElement,
|
||||
results,
|
||||
startIcon,
|
||||
endIcon,
|
||||
isCancelled,
|
||||
}: {
|
||||
element?: AnalysisResultDetailsElement;
|
||||
nestedElement?: AnalysisResultsDetailsElementNested;
|
||||
isNestedElement?: boolean;
|
||||
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
|
||||
results?: AnalysisResultForDisplay;
|
||||
isCancelled?: boolean;
|
||||
startIcon?: ReactElement | null;
|
||||
endIcon?: ReactNode | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const name = analysisElement.analysis_name_lab || '';
|
||||
const status = results?.norm_status || AnalysisStatus.NORMAL;
|
||||
const value = results?.response_value || 0;
|
||||
const unit = results?.unit || '';
|
||||
const normLowerIncluded = results?.norm_lower_included || false;
|
||||
const normUpperIncluded = results?.norm_upper_included || false;
|
||||
const normLower = results?.norm_lower || 0;
|
||||
const normUpper = results?.norm_upper || 0;
|
||||
|
||||
const element = (() => {
|
||||
if (isNestedElement) {
|
||||
return nestedElement!;
|
||||
}
|
||||
return elementOriginal!;
|
||||
})();
|
||||
const results: AnalysisResultDetailsElement['results'] = useMemo(() => {
|
||||
if (isNestedElement) {
|
||||
const nestedElement = element as AnalysisResultsDetailsElementNested;
|
||||
return {
|
||||
analysisElementOriginalId: nestedElement.analysisElementOriginalId,
|
||||
normLower: nestedElement.normLower,
|
||||
normUpper: nestedElement.normUpper,
|
||||
normStatus: nestedElement.normStatus,
|
||||
responseTime: nestedElement.responseTime,
|
||||
responseValue: nestedElement.responseValue,
|
||||
responseValueIsNegative: nestedElement.responseValueIsNegative,
|
||||
responseValueIsWithinNorm: nestedElement.responseValueIsWithinNorm,
|
||||
normLowerIncluded: nestedElement.normLowerIncluded,
|
||||
normUpperIncluded: nestedElement.normUpperIncluded,
|
||||
unit: nestedElement.unit,
|
||||
status: nestedElement.status,
|
||||
nestedElements: [],
|
||||
};
|
||||
}
|
||||
return (element as AnalysisResultDetailsElement).results;
|
||||
}, [element, isNestedElement]);
|
||||
|
||||
const name = element.analysisName || '';
|
||||
|
||||
const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null;
|
||||
const hasIsNegative = results?.responseValueIsNegative !== null;
|
||||
|
||||
const value = (() => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const analysisResultLevel = useMemo(() => {
|
||||
if (!results) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
responseValue,
|
||||
responseValueIsNegative,
|
||||
responseValueIsWithinNorm,
|
||||
} = results;
|
||||
if (responseValue === null || responseValue === undefined) {
|
||||
if (hasIsNegative) {
|
||||
if (responseValueIsNegative) {
|
||||
return t('analysis-results:results.value.negative');
|
||||
}
|
||||
return t('analysis-results:results.value.positive');
|
||||
const isUnderNorm = value < normLower;
|
||||
if (isUnderNorm) {
|
||||
switch (status) {
|
||||
case AnalysisStatus.MEDIUM:
|
||||
return AnalysisResultLevel.LOW;
|
||||
default:
|
||||
return AnalysisResultLevel.VERY_LOW;
|
||||
}
|
||||
if (hasIsWithinNorm) {
|
||||
if (responseValueIsWithinNorm) {
|
||||
return t('analysis-results:results.value.isWithinNorm');
|
||||
}
|
||||
return t('analysis-results:results.value.isNotWithinNorm');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return responseValue;
|
||||
})();
|
||||
const unit = results?.unit || '';
|
||||
const normLower = results?.normLower ?? null;
|
||||
const normUpper = results?.normUpper ?? null;
|
||||
const normStatus = results?.normStatus ?? null;
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const analysisResultLevel = useMemo(() => {
|
||||
if (normStatus === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (normStatus) {
|
||||
case 1:
|
||||
return AnalysisResultLevel.WARNING;
|
||||
case 2:
|
||||
return AnalysisResultLevel.CRITICAL;
|
||||
case 0:
|
||||
switch (status) {
|
||||
case AnalysisStatus.MEDIUM:
|
||||
return AnalysisResultLevel.HIGH;
|
||||
case AnalysisStatus.HIGH:
|
||||
return AnalysisResultLevel.VERY_HIGH;
|
||||
default:
|
||||
return AnalysisResultLevel.NORMAL;
|
||||
}
|
||||
}, [normStatus]);
|
||||
|
||||
const isCancelled = Number(results?.status) === 5;
|
||||
const nestedElements = results?.nestedElements ?? null;
|
||||
const hasNestedElements =
|
||||
Array.isArray(nestedElements) && nestedElements.length > 0;
|
||||
|
||||
const normRangeText = (() => {
|
||||
if (normLower === null && normUpper === null) {
|
||||
return null;
|
||||
}
|
||||
return `${normLower ?? '...'} - ${normUpper ?? '...'}`;
|
||||
})();
|
||||
const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
|
||||
}, [results, value, normLower]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('border-border rounded-lg border px-5', {
|
||||
'ml-8': isNestedElement,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:h-[65px] sm:flex-row sm:gap-0 sm:py-3">
|
||||
<div
|
||||
className={cn('flex items-center gap-2 font-semibold', {
|
||||
'font-bold': isNestedElement,
|
||||
})}
|
||||
>
|
||||
<div className="border-border rounded-lg border px-5">
|
||||
<div className="flex flex-col items-center justify-between gap-2 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
{startIcon || <div className="w-4" />}
|
||||
{name}
|
||||
{results?.responseTime && (
|
||||
{results?.response_time && (
|
||||
<div
|
||||
className="group/tooltip relative"
|
||||
onClick={(e) => {
|
||||
@@ -154,55 +105,42 @@ const Analysis = ({
|
||||
>
|
||||
<Trans i18nKey="analysis-results:analysisDate" />
|
||||
{': '}
|
||||
{format(new Date(results.responseTime), 'dd.MM.yyyy HH:mm')}
|
||||
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCancelled && (
|
||||
<div className="text-sm font-semibold text-red-600">
|
||||
<Trans i18nKey="analysis-results:cancelled" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCancelled || !results || hasNestedElements ? null : (
|
||||
{results ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<div
|
||||
className={cn('font-semibold', {
|
||||
'text-yellow-600':
|
||||
hasTextualResponse &&
|
||||
analysisResultLevel === AnalysisResultLevel.WARNING,
|
||||
'text-red-600':
|
||||
hasTextualResponse &&
|
||||
analysisResultLevel === AnalysisResultLevel.CRITICAL,
|
||||
'text-green-600':
|
||||
hasTextualResponse &&
|
||||
analysisResultLevel === AnalysisResultLevel.NORMAL,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<div className="font-semibold">{value}</div>
|
||||
<div className="text-muted-foreground text-sm">{unit}</div>
|
||||
</div>
|
||||
{!hasTextualResponse && (
|
||||
<>
|
||||
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
||||
{normRangeText}
|
||||
<div>
|
||||
<Trans i18nKey="analysis-results:results.range.normal" />
|
||||
</div>
|
||||
</div>
|
||||
<AnalysisLevelBar
|
||||
results={results}
|
||||
level={analysisResultLevel!}
|
||||
normRangeText={normRangeText}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
|
||||
{normLower} - {normUpper}
|
||||
<div>
|
||||
<Trans i18nKey="analysis-results:results.range.normal" />
|
||||
</div>
|
||||
</div>
|
||||
<AnalysisLevelBar
|
||||
results={results}
|
||||
normLowerIncluded={normLowerIncluded}
|
||||
normUpperIncluded={normUpperIncluded}
|
||||
level={analysisResultLevel!}
|
||||
/>
|
||||
{endIcon || <div className="mx-2 w-4" />}
|
||||
</>
|
||||
)}
|
||||
) : (isCancelled ? null : (
|
||||
<>
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<div className="font-semibold">
|
||||
<Trans i18nKey="analysis-results:waitingForResults" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-8 w-[60px]"></div>
|
||||
<AnalysisLevelBarSkeleton />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import Modal from '@modules/common/components/modal';
|
||||
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import Analysis from '../_components/analysis';
|
||||
import { analysisResponses } from './test-responses';
|
||||
|
||||
export default function AnalysisResultsPage() {
|
||||
const [openBlocks, setOpenBlocks] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
<PageBody className="gap-4">
|
||||
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
|
||||
<div>
|
||||
<h2>Analüüsi tulemused demo</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{analysisResponses.map(({ id, orderedAnalysisElements }, index) => {
|
||||
const isOpen = openBlocks.includes(id);
|
||||
const closeModal = () =>
|
||||
setOpenBlocks(openBlocks.filter((block) => block !== id));
|
||||
return (
|
||||
<div key={index} className="flex flex-col gap-2 py-4">
|
||||
<div className="flex flex-col gap-2 pb-4">
|
||||
<h3>AnalysisOrderId: {id}</h3>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h5>OrderedAnalysisElements</h5>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isOpen) {
|
||||
closeModal();
|
||||
} else {
|
||||
setOpenBlocks([...openBlocks, id]);
|
||||
}
|
||||
}}
|
||||
className="w-fit"
|
||||
color={isOpen ? 'orange' : 'grey'}
|
||||
>
|
||||
{isOpen ? 'Close' : 'Open'}
|
||||
</Button>
|
||||
{isOpen && (
|
||||
<Modal isOpen={isOpen} close={closeModal} size="large">
|
||||
<div className="overflow-y-auto">
|
||||
<p>NormiStaatus</p>
|
||||
<ul>
|
||||
<li>
|
||||
0 - testi väärtus jääb normaalväärtuste piirkonda
|
||||
või on määramata,
|
||||
</li>
|
||||
<li>
|
||||
1 - testi väärtus jääb hoiatava (tähelepanu
|
||||
suunava) märkega piirkonda,
|
||||
</li>
|
||||
<li>
|
||||
2 - testi väärtus on normaalväärtuste piirkonnast
|
||||
väljas või kõrgendatud tähelepanu nõudvas
|
||||
piirkonnas.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>UuringOlek</p>
|
||||
<ul>
|
||||
<li>1 - Järjekorras,</li>
|
||||
<li>2 - Ootel,</li>
|
||||
<li>3 - Töös,</li>
|
||||
<li>4 - Lõpetatud,</li>
|
||||
<li>5 - Tagasi lükatud,</li>
|
||||
<li>6 - Tühistatud,</li>
|
||||
</ul>
|
||||
|
||||
<pre className="bg-muted rounded-md p-4 text-sm">
|
||||
{JSON.stringify(orderedAnalysisElements, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{orderedAnalysisElements ? (
|
||||
orderedAnalysisElements.map((element, index) => (
|
||||
<Analysis key={index} element={element} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="analysis-results:noAnalysisElements" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,906 +0,0 @@
|
||||
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||
|
||||
type AnalysisTestResponse = Omit<
|
||||
AnalysisResultDetailsMapped,
|
||||
'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements'
|
||||
>;
|
||||
|
||||
const empty1: AnalysisTestResponse = {
|
||||
id: 1,
|
||||
orderedAnalysisElements: [],
|
||||
};
|
||||
|
||||
const big1: AnalysisTestResponse = {
|
||||
id: 2,
|
||||
orderedAnalysisElements: [
|
||||
{
|
||||
analysisIdOriginal: '1744-2',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'ALAT',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'U/l',
|
||||
normLower: null,
|
||||
normUpper: 45,
|
||||
normStatus: 2,
|
||||
responseTime: '2024-02-29T10:42:25+00:00',
|
||||
responseValue: 84,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1744-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '1920-8',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'ASAT',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'U/l',
|
||||
normLower: 15,
|
||||
normUpper: 45,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:20:55+00:00',
|
||||
responseValue: 45,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1920-8',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '1988-5',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'CRP',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mg/l',
|
||||
normLower: null,
|
||||
normUpper: 5,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:18:49+00:00',
|
||||
responseValue: 0.79,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1988-5',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '57747-8',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Erütrotsüüdid',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 5,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: true,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '57747-8',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '2276-4',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Ferritiin',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'µg/l',
|
||||
normLower: 28,
|
||||
normUpper: 370,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:46:54+00:00',
|
||||
responseValue: 204.1,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '2276-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '14771-0',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Glükoos',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mmol/l',
|
||||
normLower: 4.1,
|
||||
normUpper: 6,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:06:24+00:00',
|
||||
responseValue: 5.4,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14771-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '59156-0',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Glükoos',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 2,
|
||||
normStatus: 2,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: false,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '59156-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '59156-0',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Glükoos',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 2,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: true,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '59156-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '13955-0',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'HCV Ab',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: null,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T13:44:48+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: true,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '13955-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '14646-4',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'HDL kolesterool',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mmol/l',
|
||||
normLower: 1,
|
||||
normUpper: null,
|
||||
normStatus: 1,
|
||||
responseTime: '2024-02-29T10:20:55+00:00',
|
||||
responseValue: 0.8,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14646-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '2000-8',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Kaltsium',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mmol/l',
|
||||
normLower: 2.1,
|
||||
normUpper: 2.55,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:12:10+00:00',
|
||||
responseValue: 2.49,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '2000-8',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '59158-6',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Ketokehad',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 0.5,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: true,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '59158-6',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '14647-2',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Kolesterool',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mmol/l',
|
||||
normLower: null,
|
||||
normUpper: 5,
|
||||
normStatus: 1,
|
||||
responseTime: '2024-02-29T10:20:34+00:00',
|
||||
responseValue: 5.7,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14647-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '14682-9',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Kreatiniin',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'µmol/l',
|
||||
normLower: 64,
|
||||
normUpper: 111,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:19:00+00:00',
|
||||
responseValue: 89,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14682-9',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '22748-8',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'LDL kolesterool',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mmol/l',
|
||||
normLower: null,
|
||||
normUpper: 3,
|
||||
normStatus: 1,
|
||||
responseTime: '2024-02-29T10:21:15+00:00',
|
||||
responseValue: 4.3,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '22748-8',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '58805-3',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Leukotsüüdid',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 10,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: true,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '58805-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '2601-3',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Magneesium',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mmol/l',
|
||||
normLower: 0.66,
|
||||
normUpper: 1.07,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:17:26+00:00',
|
||||
responseValue: 0.82,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '2601-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '70204-3',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Mitte-HDL kolesterool',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
labComment: 'Mitte-paastu veri <3,9 mmol/L',
|
||||
unit: 'mmol/l',
|
||||
normLower: null,
|
||||
normUpper: 3.8,
|
||||
normStatus: 1,
|
||||
responseTime: '2024-02-29T10:20:55+00:00',
|
||||
responseValue: 4.9,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '70204-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '14798-3',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Raud',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'µmol/l',
|
||||
normLower: 11.6,
|
||||
normUpper: 31.3,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:21:16+00:00',
|
||||
responseValue: 16.5,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14798-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '14927-8',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Triglütseriidid',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
labComment: 'Mitte-paastu veri <2,0 mmol/L',
|
||||
unit: 'mmol/l',
|
||||
normLower: null,
|
||||
normUpper: 1.7,
|
||||
normStatus: 1,
|
||||
responseTime: '2024-02-29T10:21:16+00:00',
|
||||
responseValue: 1.89,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '14927-8',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '3016-3',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'TSH',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mIU/l',
|
||||
normLower: 0.4,
|
||||
normUpper: 4,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:49:02+00:00',
|
||||
responseValue: 1.27,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '3016-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '22664-7',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Uurea',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mmol/l',
|
||||
normLower: 3.2,
|
||||
normUpper: 7.4,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:19:11+00:00',
|
||||
responseValue: 6.4,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '22664-7',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '50561-0',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Valk',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 0.25,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: true,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '50561-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '60493-4',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Vitamiin D (25-OH)',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
labComment: 'Väärtus >75 nmol/l on D-vitamiini tervislik tase',
|
||||
unit: 'nmol/l',
|
||||
normLower: 75,
|
||||
normUpper: null,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:49:22+00:00',
|
||||
responseValue: 105.5,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '60493-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '60025-4',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Urobilinogeen',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: 17,
|
||||
normStatus: 0,
|
||||
responseTime: '2024-02-29T10:13:01+00:00',
|
||||
responseValue: null,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: true,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '60025-4',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const big2: AnalysisTestResponse = {
|
||||
id: 3,
|
||||
orderedAnalysisElements: [
|
||||
{
|
||||
analysisIdOriginal: '1988-5',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'CRP',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
unit: 'mg/L',
|
||||
normLower: null,
|
||||
normUpper: 5,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12T14:02:04+00:00',
|
||||
responseValue: 1,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '1988-5',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '57021-8',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Hemogramm',
|
||||
results: {
|
||||
nestedElements: [
|
||||
{
|
||||
status: 4,
|
||||
unit: 'g/L',
|
||||
normLower: 134,
|
||||
normUpper: 170,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:03',
|
||||
responseValue: 150,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '718-7',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: '%',
|
||||
normLower: 40,
|
||||
normUpper: 49,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:03',
|
||||
responseValue: 45,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '4544-3',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 4.1,
|
||||
normUpper: 9.7,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:03',
|
||||
responseValue: 5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '6690-2',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E12/L',
|
||||
normLower: 4.5,
|
||||
normUpper: 5.7,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:03',
|
||||
responseValue: 5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '789-8',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'fL',
|
||||
normLower: 82,
|
||||
normUpper: 95,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 85,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '787-2',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'pg',
|
||||
normLower: 28,
|
||||
normUpper: 33,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 30,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '785-6',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'g/L',
|
||||
normLower: 322,
|
||||
normUpper: 356,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 355,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '786-4',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: '%',
|
||||
normLower: 12,
|
||||
normUpper: 15,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 15,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '788-0',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 157,
|
||||
normUpper: 372,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 255,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '777-3',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: '%',
|
||||
normLower: 0.18,
|
||||
normUpper: 0.38,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0.2,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '51637-7',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'fL',
|
||||
normLower: 9.2,
|
||||
normUpper: 12.3,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 10,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '32623-1',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'fL',
|
||||
normLower: 10.1,
|
||||
normUpper: 16.2,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 15,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '32207-3',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 0.01,
|
||||
normUpper: 0.08,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0.05,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '704-7',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 0.02,
|
||||
normUpper: 0.4,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0.05,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '711-2',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 1.9,
|
||||
normUpper: 6.7,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '751-8',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 0.24,
|
||||
normUpper: 0.8,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0.5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '742-7',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 1.3,
|
||||
normUpper: 3.1,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 1.5,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '731-0',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normLower: 0,
|
||||
normUpper: 0.03,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:03',
|
||||
responseValue: 0,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '51584-1',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: '%',
|
||||
normLower: 0,
|
||||
normUpper: 0.5,
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '38518-7',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: 'E9/L',
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0,
|
||||
normUpper: null,
|
||||
normLower: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '771-6',
|
||||
},
|
||||
{
|
||||
status: 4,
|
||||
unit: '/100WBC',
|
||||
normStatus: 0,
|
||||
responseTime: '2025-09-12 14:02:04',
|
||||
responseValue: 0,
|
||||
normUpper: null,
|
||||
normLower: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
analysisElementOriginalId: '58413-6',
|
||||
},
|
||||
],
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: null,
|
||||
normStatus: null,
|
||||
responseTime: null,
|
||||
responseValue: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: false,
|
||||
responseValueIsWithinNorm: false,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '57021-8',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '43583-4',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Lipoproteiin a',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
labComment: 'Kliendi soovil analüüs tühistatud.',
|
||||
unit: null,
|
||||
normLower: null,
|
||||
normUpper: null,
|
||||
normStatus: null,
|
||||
responseTime: null,
|
||||
responseValue: null,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: false,
|
||||
responseValueIsWithinNorm: false,
|
||||
status: 5,
|
||||
analysisElementOriginalId: '43583-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
analysisIdOriginal: '60493-4',
|
||||
isWaitingForResults: false,
|
||||
analysisName: 'Vitamiin D (25-OH)',
|
||||
results: {
|
||||
nestedElements: [],
|
||||
labComment:
|
||||
'Väärtus vahemikus 30-49.9 nmol/L on D-vitamiini ebapiisav tase.',
|
||||
unit: 'nmol/L',
|
||||
normLower: 75,
|
||||
normUpper: null,
|
||||
normStatus: 1,
|
||||
responseTime: '2025-09-12T14:02:04+00:00',
|
||||
responseValue: 30,
|
||||
normLowerIncluded: false,
|
||||
normUpperIncluded: false,
|
||||
responseValueIsNegative: null,
|
||||
responseValueIsWithinNorm: null,
|
||||
status: 4,
|
||||
analysisElementOriginalId: '60493-4',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const analysisResponses: AnalysisTestResponse[] = [empty1, big1, big2];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user