Merge branch 'develop' into feature/MED-129
This commit is contained in:
1
.npmrc
1
.npmrc
@@ -3,6 +3,7 @@ dedupe-peer-dependents=true
|
|||||||
use-lockfile-v6=true
|
use-lockfile-v6=true
|
||||||
resolution-mode=highest
|
resolution-mode=highest
|
||||||
package-manager-strict=true
|
package-manager-strict=true
|
||||||
|
ignore-workspace-root-check=true
|
||||||
public-hoist-pattern[]=*i18next*
|
public-hoist-pattern[]=*i18next*
|
||||||
public-hoist-pattern[]=*eslint*
|
public-hoist-pattern[]=*eslint*
|
||||||
public-hoist-pattern[]=*prettier*
|
public-hoist-pattern[]=*prettier*
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -47,6 +47,18 @@ if you get missing dependency error do `pnpm i --force`
|
|||||||
pnpm add <pacakge-name> -w
|
pnpm add <pacakge-name> -w
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Eslint and prettier
|
||||||
|
|
||||||
|
To clean code run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint:fix
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run prettier:fix
|
||||||
|
```
|
||||||
|
|
||||||
## Supabase
|
## Supabase
|
||||||
|
|
||||||
Start supabase in docker
|
Start supabase in docker
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,14 @@ import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
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 { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
|
||||||
import { useAuthConfig } from '@kit/shared/hooks';
|
|
||||||
|
|
||||||
const ModeToggle = dynamic(() =>
|
const ModeToggle = dynamic(() =>
|
||||||
import('@kit/ui/mode-toggle').then((mod) => ({
|
import('@kit/ui/mode-toggle').then((mod) => ({
|
||||||
default: mod.ModeToggle,
|
default: mod.ModeToggle,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
|
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
|
||||||
|
|
||||||
import { Header } from '@kit/ui/marketing';
|
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
|
import { Header } from '@kit/ui/marketing';
|
||||||
|
|
||||||
import { SiteHeaderAccountSection } from './site-header-account-section';
|
import { SiteHeaderAccountSection } from './site-header-account-section';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { createCmsClient } from '@kit/cms';
|
|||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
import { Post } from '../../blog/_components/post';
|
import { Post } from '../../blog/_components/post';
|
||||||
|
|
||||||
interface BlogPageProps {
|
interface BlogPageProps {
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ const contactEmail = z
|
|||||||
.string({
|
.string({
|
||||||
error:
|
error:
|
||||||
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
|
'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);
|
.parse(process.env.CONTACT_EMAIL);
|
||||||
|
|
||||||
const emailFrom = z
|
const emailFrom = z
|
||||||
.string({
|
.string({
|
||||||
error:
|
error:
|
||||||
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
'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);
|
.parse(process.env.EMAIL_SENDER);
|
||||||
|
|
||||||
export const sendContactEmail = enhanceAction(
|
export const sendContactEmail = enhanceAction(
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { use } from 'react';
|
|||||||
|
|
||||||
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
||||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
import { loadCurrentUserAccounts } from '~/home/(user)/_lib/server/load-accounts';
|
import { loadCurrentUserAccounts } from '~/home/(user)/_lib/server/load-accounts';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
function SiteLayout(props: React.PropsWithChildren) {
|
function SiteLayout(props: React.PropsWithChildren) {
|
||||||
const accounts = use(loadCurrentUserAccounts());
|
const accounts = use(loadCurrentUserAccounts());
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ArrowRightIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { ArrowRightIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import { CtaButton, Hero } from '@kit/ui/marketing';
|
import { CtaButton, Hero } from '@kit/ui/marketing';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import CompanyOfferForm from './_components/company-offer-form';
|
import CompanyOfferForm from './_components/company-offer-form';
|
||||||
|
|
||||||
function CompanyOffer() {
|
function CompanyOffer() {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
function SiteLayout(props: React.PropsWithChildren) {
|
function SiteLayout(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex min-h-[100vh] flex-col justify-center items-center'}>
|
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
24
app/admin/_components/admin-menu-navigation.tsx
Normal file
24
app/admin/_components/admin-menu-navigation.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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,73 +1,25 @@
|
|||||||
'use client';
|
import { adminNavigationConfig } from '@kit/shared/config';
|
||||||
|
|
||||||
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 {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarNavigation,
|
||||||
SidebarMenuButton,
|
|
||||||
useSidebar,
|
|
||||||
} from '@kit/ui/shadcn-sidebar';
|
} from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
export function AdminSidebar() {
|
||||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
const collapsible = adminNavigationConfig.sidebarCollapsedStyle;
|
||||||
|
|
||||||
export function AdminSidebar({
|
|
||||||
accounts,
|
|
||||||
}: {
|
|
||||||
accounts: UserWorkspace['accounts'];
|
|
||||||
}) {
|
|
||||||
const path = usePathname();
|
|
||||||
const { open } = useSidebar();
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible={collapsible}>
|
||||||
<SidebarHeader className={'m-2'}>
|
<SidebarHeader className="h-24 justify-center">
|
||||||
<AppLogo href={'/admin'} className="max-w-full" compact={!open} />
|
<div className="mt-24 flex items-center">
|
||||||
|
<h5>Superadmin</h5>
|
||||||
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarNavigation config={adminNavigationConfig} />
|
||||||
<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>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
|
||||||
<ProfileAccountDropdownContainer accounts={accounts} />
|
|
||||||
</SidebarFooter>
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { cache } from 'react';
|
|||||||
|
|
||||||
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
||||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import { getAccount } from '~/lib/services/account.service';
|
import { getAccount } from '~/lib/services/account.service';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
@@ -26,6 +28,6 @@ async function AccountPage(props: Params) {
|
|||||||
return <AdminAccountPage account={account} />;
|
return <AdminAccountPage account={account} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminGuard(AccountPage);
|
export default withI18n(AdminGuard(AccountPage));
|
||||||
|
|
||||||
const loadAccount = cache(getAccount);
|
const loadAccount = cache(getAccount);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
|||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
interface SearchParams {
|
interface SearchParams {
|
||||||
page?: string;
|
page?: string;
|
||||||
account_type?: 'all' | 'team' | 'personal';
|
account_type?: 'all' | 'team' | 'personal';
|
||||||
@@ -30,7 +32,7 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader description={<AppBreadcrumbs />}>
|
<PageHeader description={<AppBreadcrumbs />} title="Accounts">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<AdminCreateUserDialog>
|
<AdminCreateUserDialog>
|
||||||
<Button data-test="admin-create-user-button">
|
<Button data-test="admin-create-user-button">
|
||||||
@@ -84,4 +86,4 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminGuard(AccountsPage);
|
export default withI18n(AdminGuard(AccountsPage));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { cookies } from 'next/headers';
|
|||||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
|
import { AdminMenuNavigation } from '~/admin/_components/admin-menu-navigation';
|
||||||
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||||
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
|
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
|
||||||
|
|
||||||
@@ -21,19 +22,24 @@ export default function AdminLayout(props: React.PropsWithChildren) {
|
|||||||
const workspace = use(loadUserWorkspace());
|
const workspace = use(loadUserWorkspace());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider defaultOpen={state.open}>
|
<Page style={'header'}>
|
||||||
<Page style={'sidebar'}>
|
|
||||||
<PageNavigation>
|
<PageNavigation>
|
||||||
<AdminSidebar accounts={workspace.accounts} />
|
<AdminMenuNavigation workspace={workspace} />
|
||||||
</PageNavigation>
|
</PageNavigation>
|
||||||
|
|
||||||
<PageMobileNavigation>
|
<PageMobileNavigation>
|
||||||
<AdminMobileNavigation />
|
<AdminMobileNavigation />
|
||||||
</PageMobileNavigation>
|
</PageMobileNavigation>
|
||||||
|
|
||||||
|
<SidebarProvider defaultOpen={state.open}>
|
||||||
|
<Page style={'sidebar'}>
|
||||||
|
<PageNavigation>
|
||||||
|
<AdminSidebar />
|
||||||
|
</PageNavigation>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Page>
|
</Page>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { AdminDashboard } from '@kit/admin/components/admin-dashboard';
|
|||||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
function AdminPage() {
|
function AdminPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader description={`Super Admin`} />
|
<PageHeader title={`Super Admin`} />
|
||||||
|
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<AdminDashboard />
|
<AdminDashboard />
|
||||||
@@ -14,4 +16,4 @@ function AdminPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminGuard(AdminPage);
|
export default withI18n(AdminGuard(AdminPage));
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { getPlanTypesMap } from '@kit/billing';
|
import { getPlanTypesMap } from '@kit/billing';
|
||||||
import { getBillingEventHandlerService } from '@kit/billing-gateway';
|
import { getBillingEventHandlerService } from '@kit/billing-gateway';
|
||||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||||
|
import { billingConfig } from '@kit/shared/config';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
|
|
||||||
import { billingConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Handle the webhooks from Stripe related to checkouts
|
* @description Handle the webhooks from Stripe related to checkouts
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { config } from 'dotenv';
|
|||||||
|
|
||||||
export default function loadEnv() {
|
export default function loadEnv() {
|
||||||
config({ path: `.env` });
|
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}` });
|
config({ path: `.env.${process.env.NODE_ENV}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import Medusa from "@medusajs/js-sdk"
|
import { listProductTypes } from '@lib/data/products';
|
||||||
import type { AdminProductCategory } from "@medusajs/types";
|
import Medusa from '@medusajs/js-sdk';
|
||||||
import { listProductTypes } from "@lib/data/products";
|
import type { AdminProductCategory } from '@medusajs/types';
|
||||||
import { getAnalysisElements } from "~/lib/services/analysis-element.service";
|
|
||||||
import { getAnalysisGroups } from "~/lib/services/analysis-group.service";
|
import {
|
||||||
import { createMedusaSyncFailEntry, createMedusaSyncSuccessEntry } from "~/lib/services/analyses.service";
|
createMedusaSyncFailEntry,
|
||||||
|
createMedusaSyncSuccessEntry,
|
||||||
|
} from '~/lib/services/analyses.service';
|
||||||
|
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
|
||||||
|
import { getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
||||||
|
|
||||||
const SYNLAB_SERVICES_CATEGORY_HANDLE = 'synlab-services';
|
const SYNLAB_SERVICES_CATEGORY_HANDLE = 'synlab-services';
|
||||||
const SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE = 'synlab-analysis';
|
const SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE = 'synlab-analysis';
|
||||||
@@ -12,7 +16,8 @@ const BASE_ANALYSIS_PRODUCT_HANDLE = 'analysis-base';
|
|||||||
|
|
||||||
const getAdminSdk = () => {
|
const getAdminSdk = () => {
|
||||||
const medusaBackendUrl = process.env.MEDUSA_BACKEND_PUBLIC_URL!;
|
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!;
|
const key = process.env.MEDUSA_SECRET_API_KEY!;
|
||||||
|
|
||||||
if (!medusaBackendUrl || !medusaPublishableApiKey) {
|
if (!medusaBackendUrl || !medusaPublishableApiKey) {
|
||||||
@@ -23,15 +28,14 @@ const getAdminSdk = () => {
|
|||||||
debug: process.env.NODE_ENV === 'development',
|
debug: process.env.NODE_ENV === 'development',
|
||||||
apiKey: key,
|
apiKey: key,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
async function createProductCategories({
|
async function createProductCategories({ medusa }: { medusa: Medusa }) {
|
||||||
medusa,
|
const { product_categories: existingProductCategories } =
|
||||||
}: {
|
await medusa.admin.productCategory.list();
|
||||||
medusa: Medusa;
|
const parentCategory = existingProductCategories.find(
|
||||||
}) {
|
({ handle }) => handle === SYNLAB_SERVICES_CATEGORY_HANDLE,
|
||||||
const { product_categories: existingProductCategories } = await medusa.admin.productCategory.list();
|
);
|
||||||
const parentCategory = existingProductCategories.find(({ handle }) => handle === SYNLAB_SERVICES_CATEGORY_HANDLE);
|
|
||||||
|
|
||||||
if (!parentCategory) {
|
if (!parentCategory) {
|
||||||
throw new Error('Parent category not found');
|
throw new Error('Parent category not found');
|
||||||
@@ -46,8 +50,12 @@ async function createProductCategories({
|
|||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
console.info(`Processing analysis group '${analysisGroup.name}'`);
|
console.info(`Processing analysis group '${analysisGroup.name}'`);
|
||||||
|
|
||||||
const isExisting = existingProductCategories.find(({ name }) => name === analysisGroup.name);
|
const isExisting = existingProductCategories.find(
|
||||||
const isNewlyCreated = createdCategories.find(({ name }) => name === analysisGroup.name);
|
({ name }) => name === analysisGroup.name,
|
||||||
|
);
|
||||||
|
const isNewlyCreated = createdCategories.find(
|
||||||
|
({ name }) => name === analysisGroup.name,
|
||||||
|
);
|
||||||
if (isExisting || isNewlyCreated) {
|
if (isExisting || isNewlyCreated) {
|
||||||
console.info(`Analysis group '${analysisGroup.name}' already exists`);
|
console.info(`Analysis group '${analysisGroup.name}' already exists`);
|
||||||
continue;
|
continue;
|
||||||
@@ -63,18 +71,19 @@ async function createProductCategories({
|
|||||||
analysisGroupId: analysisGroup.id,
|
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);
|
createdCategories.push(createResponse.product_category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getChildProductCategories({
|
async function getChildProductCategories({ medusa }: { medusa: Medusa }) {
|
||||||
medusa,
|
const { product_categories: allCategories } =
|
||||||
}: {
|
await medusa.admin.productCategory.list();
|
||||||
medusa: Medusa;
|
const childCategories = allCategories.filter(
|
||||||
}) {
|
({ parent_category_id }) => parent_category_id !== null,
|
||||||
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
|
);
|
||||||
const childCategories = allCategories.filter(({ parent_category_id }) => parent_category_id !== null);
|
|
||||||
return childCategories;
|
return childCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,34 +102,34 @@ async function deleteProductCategories({
|
|||||||
/**
|
/**
|
||||||
* In case a reset is needed
|
* In case a reset is needed
|
||||||
*/
|
*/
|
||||||
async function deleteProducts({
|
async function deleteProducts({ medusa }: { medusa: Medusa }) {
|
||||||
medusa,
|
|
||||||
}: {
|
|
||||||
medusa: Medusa;
|
|
||||||
}) {
|
|
||||||
const { products: existingProducts } = await medusa.admin.product.list({
|
const { products: existingProducts } = await medusa.admin.product.list({
|
||||||
fields: 'id,collection_id',
|
fields: 'id,collection_id',
|
||||||
limit: 1000,
|
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() {
|
async function getAnalysisPackagesType() {
|
||||||
const { productTypes } = await listProductTypes();
|
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) {
|
if (!analysisPackagesType) {
|
||||||
throw new Error('Synlab analysis packages type not found');
|
throw new Error('Synlab analysis packages type not found');
|
||||||
}
|
}
|
||||||
return analysisPackagesType;
|
return analysisPackagesType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProductDefaultFields({
|
async function getProductDefaultFields({ medusa }: { medusa: Medusa }) {
|
||||||
medusa,
|
const baseProductsResponse = await medusa.admin.product.list({
|
||||||
}: {
|
handle: BASE_ANALYSIS_PRODUCT_HANDLE,
|
||||||
medusa: Medusa;
|
});
|
||||||
}) {
|
|
||||||
const baseProductsResponse = await medusa.admin.product.list({ handle: BASE_ANALYSIS_PRODUCT_HANDLE })
|
|
||||||
const baseProduct = baseProductsResponse.products[0];
|
const baseProduct = baseProductsResponse.products[0];
|
||||||
if (!baseProduct) {
|
if (!baseProduct) {
|
||||||
throw new Error('Base product not found');
|
throw new Error('Base product not found');
|
||||||
@@ -142,25 +151,18 @@ async function getProductDefaultFields({
|
|||||||
defaultSalesChannels,
|
defaultSalesChannels,
|
||||||
defaultProductOption,
|
defaultProductOption,
|
||||||
defaultProductVariant,
|
defaultProductVariant,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createProducts({
|
async function createProducts({ medusa }: { medusa: Medusa }) {
|
||||||
medusa,
|
const { product_categories: allCategories } =
|
||||||
}: {
|
await medusa.admin.productCategory.list();
|
||||||
medusa: Medusa;
|
|
||||||
}) {
|
|
||||||
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ products: existingProducts },
|
{ products: existingProducts },
|
||||||
analysisElements,
|
analysisElements,
|
||||||
analysisPackagesType,
|
analysisPackagesType,
|
||||||
{
|
{ defaultSalesChannels, defaultProductOption, defaultProductVariant },
|
||||||
defaultSalesChannels,
|
|
||||||
defaultProductOption,
|
|
||||||
defaultProductVariant,
|
|
||||||
}
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
medusa.admin.product.list({
|
medusa.admin.product.list({
|
||||||
category_id: allCategories.map(({ id }) => id),
|
category_id: allCategories.map(({ id }) => id),
|
||||||
@@ -168,13 +170,17 @@ async function createProducts({
|
|||||||
getAnalysisElements({ getAll: true }),
|
getAnalysisElements({ getAll: true }),
|
||||||
getAnalysisPackagesType(),
|
getAnalysisPackagesType(),
|
||||||
getProductDefaultFields({ medusa }),
|
getProductDefaultFields({ medusa }),
|
||||||
])
|
]);
|
||||||
|
|
||||||
for (const analysisElement of analysisElements) {
|
for (const analysisElement of analysisElements) {
|
||||||
const { analysis_id_original: originalId } = analysisElement;
|
const { analysis_id_original: originalId } = analysisElement;
|
||||||
const isExisting = existingProducts.find(({ metadata }) => metadata?.analysisIdOriginal === originalId);
|
const isExisting = existingProducts.find(
|
||||||
|
({ metadata }) => metadata?.analysisIdOriginal === originalId,
|
||||||
|
);
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
console.info(`Analysis element '${analysisElement.analysis_name_lab}' already exists`);
|
console.info(
|
||||||
|
`Analysis element '${analysisElement.analysis_name_lab}' already exists`,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { analysis_name_lab: name } = analysisElement;
|
const { analysis_name_lab: name } = analysisElement;
|
||||||
@@ -183,7 +189,10 @@ async function createProducts({
|
|||||||
continue;
|
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) {
|
if (!category) {
|
||||||
console.error(`Category not found for analysis element '${name}'`);
|
console.error(`Category not found for analysis element '${name}'`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { XMLParser } from 'fast-xml-parser';
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
|
||||||
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types';
|
|
||||||
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry, getAnalyses } from '~/lib/services/analyses.service';
|
|
||||||
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
|
|
||||||
import { AnalysisElement, createAnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service';
|
|
||||||
import { createCodes } from '~/lib/services/codes.service';
|
|
||||||
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
|
|
||||||
import type { ICode } from '~/lib/types/code';
|
|
||||||
import { toArray } from '@kit/shared/utils';
|
import { toArray } from '@kit/shared/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAnalysis,
|
||||||
|
createNoDataReceivedEntry,
|
||||||
|
createNoNewDataReceivedEntry,
|
||||||
|
createSyncFailEntry,
|
||||||
|
createSyncSuccessEntry,
|
||||||
|
getAnalyses,
|
||||||
|
} from '~/lib/services/analyses.service';
|
||||||
|
import {
|
||||||
|
AnalysisElement,
|
||||||
|
createAnalysisElement,
|
||||||
|
getAnalysisElements,
|
||||||
|
} from '~/lib/services/analysis-element.service';
|
||||||
|
import {
|
||||||
|
createAnalysisGroup,
|
||||||
|
getAnalysisGroups,
|
||||||
|
} from '~/lib/services/analysis-group.service';
|
||||||
|
import { createCodes } from '~/lib/services/codes.service';
|
||||||
|
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types';
|
||||||
|
import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service';
|
||||||
|
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
|
||||||
|
import type { ICode } from '~/lib/types/code';
|
||||||
|
|
||||||
const WRITE_XML_TO_FILE = false as boolean;
|
const WRITE_XML_TO_FILE = false as boolean;
|
||||||
|
|
||||||
export default async function syncAnalysisGroups() {
|
export default async function syncAnalysisGroups() {
|
||||||
@@ -52,7 +68,8 @@ export default async function syncAnalysisGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parser = new XMLParser({ ignoreAttributes: false });
|
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) {
|
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -77,12 +94,18 @@ export default async function syncAnalysisGroups() {
|
|||||||
|
|
||||||
const codes: ICode[] = [];
|
const codes: ICode[] = [];
|
||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
|
const existingAnalysisGroup = existingAnalysisGroups?.find(
|
||||||
|
({ original_id }) => original_id === analysisGroup.UuringuGruppId,
|
||||||
|
);
|
||||||
let groupExistingAnalysisElements: AnalysisElement[] = [];
|
let groupExistingAnalysisElements: AnalysisElement[] = [];
|
||||||
let analysisGroupId: number;
|
let analysisGroupId: number;
|
||||||
if (existingAnalysisGroup) {
|
if (existingAnalysisGroup) {
|
||||||
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`);
|
console.info(
|
||||||
groupExistingAnalysisElements = await getAnalysisElements({ analysisGroupId: existingAnalysisGroup.id });
|
`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`,
|
||||||
|
);
|
||||||
|
groupExistingAnalysisElements = await getAnalysisElements({
|
||||||
|
analysisGroupId: existingAnalysisGroup.id,
|
||||||
|
});
|
||||||
analysisGroupId = existingAnalysisGroup.id;
|
analysisGroupId = existingAnalysisGroup.id;
|
||||||
} else {
|
} else {
|
||||||
analysisGroupId = await createAnalysisGroup({
|
analysisGroupId = await createAnalysisGroup({
|
||||||
@@ -109,10 +132,14 @@ export default async function syncAnalysisGroups() {
|
|||||||
|
|
||||||
for (const item of analysisGroupItems) {
|
for (const item of analysisGroupItems) {
|
||||||
const analysisElement = item.UuringuElement!;
|
const analysisElement = item.UuringuElement!;
|
||||||
const isExistingAnalysisElement = groupExistingAnalysisElements
|
const isExistingAnalysisElement = groupExistingAnalysisElements.find(
|
||||||
.find(({ analysis_id_original }) => analysis_id_original === analysisElement.UuringId);
|
({ analysis_id_original }) =>
|
||||||
|
analysis_id_original === analysisElement.UuringId,
|
||||||
|
);
|
||||||
if (isExistingAnalysisElement) {
|
if (isExistingAnalysisElement) {
|
||||||
console.info(`Analysis element '${analysisElement.UuringNimi}' already exists`);
|
console.info(
|
||||||
|
`Analysis element '${analysisElement.UuringNimi}' already exists`,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,16 +176,24 @@ export default async function syncAnalysisGroups() {
|
|||||||
|
|
||||||
const analyses = analysisElement.UuringuElement;
|
const analyses = analysisElement.UuringuElement;
|
||||||
if (analyses?.length) {
|
if (analyses?.length) {
|
||||||
const existingAnalyses = await getAnalyses({ originalIds: analyses.map(({ UuringId }) => UuringId) });
|
const existingAnalyses = await getAnalyses({
|
||||||
|
originalIds: analyses.map(({ UuringId }) => UuringId),
|
||||||
|
});
|
||||||
|
|
||||||
for (const analysis of analyses) {
|
for (const analysis of analyses) {
|
||||||
const isExistingAnalysis = existingAnalyses.find(({ analysis_id_original }) => analysis_id_original === analysis.UuringId);
|
const isExistingAnalysis = existingAnalyses.find(
|
||||||
|
({ analysis_id_original }) =>
|
||||||
|
analysis_id_original === analysis.UuringId,
|
||||||
|
);
|
||||||
if (isExistingAnalysis) {
|
if (isExistingAnalysis) {
|
||||||
console.info(`Analysis '${analysis.UuringNimi}' already exists`);
|
console.info(`Analysis '${analysis.UuringNimi}' already exists`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertedAnalysisId = await createAnalysis(analysis, analysisGroupId);
|
const insertedAnalysisId = await createAnalysis(
|
||||||
|
analysis,
|
||||||
|
analysisGroupId,
|
||||||
|
);
|
||||||
|
|
||||||
if (analysis.Kood) {
|
if (analysis.Kood) {
|
||||||
const analysisCodes = toArray(analysis.Kood);
|
const analysisCodes = toArray(analysis.Kood);
|
||||||
@@ -187,11 +222,17 @@ export default async function syncAnalysisGroups() {
|
|||||||
await createSyncSuccessEntry();
|
await createSyncSuccessEntry();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
await createSyncFailEntry(JSON.stringify({
|
await createSyncFailEntry(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
stack: e instanceof Error ? e.stack : undefined,
|
stack: e instanceof Error ? e.stack : undefined,
|
||||||
name: e instanceof Error ? e.name : 'Unknown',
|
name: e instanceof Error ? e.name : 'Unknown',
|
||||||
}, null, 2));
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
console.error('Sync failed:', e);
|
console.error('Sync failed:', e);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to sync public message data, error: ${errorMessage}`,
|
`Failed to sync public message data, error: ${errorMessage}`,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readPrivateMessageResponse } from "~/lib/services/medipost/medipostPrivateMessage.service";
|
import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
||||||
|
|
||||||
type ProcessedMessage = {
|
type ProcessedMessage = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
@@ -15,9 +15,9 @@ type GroupedResults = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function syncAnalysisResults() {
|
export default async function syncAnalysisResults() {
|
||||||
console.info("Syncing analysis results");
|
console.info('Syncing analysis results');
|
||||||
|
|
||||||
let processedMessages: ProcessedMessage[] = [];
|
const processedMessages: ProcessedMessage[] = [];
|
||||||
const excludedMessageIds: string[] = [];
|
const excludedMessageIds: string[] = [];
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await readPrivateMessageResponse({ excludedMessageIds });
|
const result = await readPrivateMessageResponse({ excludedMessageIds });
|
||||||
@@ -26,7 +26,7 @@ export default async function syncAnalysisResults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!result.messageId) {
|
if (!result.messageId) {
|
||||||
console.info("No more messages to process");
|
console.info('No more messages to process');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,5 +59,7 @@ export default async function syncAnalysisResults() {
|
|||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as GroupedResults);
|
}, {} as GroupedResults);
|
||||||
console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`);
|
console.info(
|
||||||
|
`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
export default function validateApiKey(request: NextRequest) {
|
export default function validateApiKey(request: NextRequest) {
|
||||||
const envApiKey = process.env.JOBS_API_TOKEN;
|
const envApiKey = process.env.JOBS_API_TOKEN;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import loadEnv from "../handler/load-env";
|
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
import { retrieveOrder } from '@lib/data/orders';
|
||||||
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
|
|
||||||
import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service";
|
import { getMedipostDispatchTries } from '~/lib/services/audit.service';
|
||||||
import { retrieveOrder } from "@lib/data/orders";
|
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
||||||
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||||
|
|
||||||
|
import loadEnv from '../handler/load-env';
|
||||||
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|
||||||
export const POST = async (request: NextRequest) => {
|
export const POST = async (request: NextRequest) => {
|
||||||
loadEnv();
|
loadEnv();
|
||||||
@@ -19,23 +22,34 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
const tries = await getMedipostDispatchTries(medusaOrderId);
|
const tries = await getMedipostDispatchTries(medusaOrderId);
|
||||||
if (tries >= 3) {
|
if (tries >= 3) {
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Order has been retried too many times',
|
message: 'Order has been retried too many times',
|
||||||
}, { status: 400 });
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const medusaOrder = await retrieveOrder(medusaOrderId);
|
const medusaOrder = await retrieveOrder(medusaOrderId);
|
||||||
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
|
const orderedAnalysisElements = await getOrderedAnalysisIds({
|
||||||
|
medusaOrder,
|
||||||
|
});
|
||||||
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||||
console.info("Successfully sent order to medipost");
|
console.info('Successfully sent order to medipost');
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Successfully sent order to medipost',
|
message: 'Successfully sent order to medipost',
|
||||||
}, { status: 200 });
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error sending order to medipost", e);
|
console.error('Error sending order to medipost', e);
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Failed to send order to medipost',
|
message: 'Failed to send order to medipost',
|
||||||
}, { status: 500 });
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
NotificationAction,
|
NotificationAction,
|
||||||
createNotificationLog,
|
createNotificationLog,
|
||||||
} from '~/lib/services/audit/notificationEntries.service';
|
} from '~/lib/services/audit/notificationEntries.service';
|
||||||
|
|
||||||
import loadEnv from '../handler/load-env';
|
import loadEnv from '../handler/load-env';
|
||||||
import sendOpenJobsEmails from '../handler/send-open-jobs-emails';
|
import sendOpenJobsEmails from '../handler/send-open-jobs-emails';
|
||||||
import validateApiKey from '../handler/validate-api-key';
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import loadEnv from "../handler/load-env";
|
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
import loadEnv from '../handler/load-env';
|
||||||
import syncAnalysisGroupsStore from "../handler/sync-analysis-groups-store";
|
import syncAnalysisGroupsStore from '../handler/sync-analysis-groups-store';
|
||||||
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|
||||||
export const POST = async (request: NextRequest) => {
|
export const POST = async (request: NextRequest) => {
|
||||||
loadEnv();
|
loadEnv();
|
||||||
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await syncAnalysisGroupsStore();
|
await syncAnalysisGroupsStore();
|
||||||
console.info("Successfully synced analysis groups store");
|
console.info('Successfully synced analysis groups store');
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Successfully synced analysis groups store',
|
message: 'Successfully synced analysis groups store',
|
||||||
}, { status: 200 });
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error syncing analysis groups store", e);
|
console.error('Error syncing analysis groups store', e);
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Failed to sync analysis groups store',
|
message: 'Failed to sync analysis groups store',
|
||||||
}, { status: 500 });
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import syncAnalysisGroups from "../handler/sync-analysis-groups";
|
|
||||||
import loadEnv from "../handler/load-env";
|
import loadEnv from '../handler/load-env';
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
import syncAnalysisGroups from '../handler/sync-analysis-groups';
|
||||||
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|
||||||
export const POST = async (request: NextRequest) => {
|
export const POST = async (request: NextRequest) => {
|
||||||
loadEnv();
|
loadEnv();
|
||||||
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await syncAnalysisGroups();
|
await syncAnalysisGroups();
|
||||||
console.info("Successfully synced analysis groups");
|
console.info('Successfully synced analysis groups');
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Successfully synced analysis groups',
|
message: 'Successfully synced analysis groups',
|
||||||
}, { status: 200 });
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error syncing analysis groups", e);
|
console.error('Error syncing analysis groups', e);
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Failed to sync analysis groups',
|
message: 'Failed to sync analysis groups',
|
||||||
}, { status: 500 });
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import loadEnv from "../handler/load-env";
|
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
import loadEnv from '../handler/load-env';
|
||||||
import syncAnalysisResults from "../handler/sync-analysis-results";
|
import syncAnalysisResults from '../handler/sync-analysis-results';
|
||||||
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|
||||||
export const POST = async (request: NextRequest) => {
|
export const POST = async (request: NextRequest) => {
|
||||||
loadEnv();
|
loadEnv();
|
||||||
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await syncAnalysisResults();
|
await syncAnalysisResults();
|
||||||
console.info("Successfully synced analysis results");
|
console.info('Successfully synced analysis results');
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Successfully synced analysis results',
|
message: 'Successfully synced analysis results',
|
||||||
}, { status: 200 });
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error syncing analysis results", e);
|
console.error('Error syncing analysis results', e);
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Failed to sync analysis results',
|
message: 'Failed to sync analysis results',
|
||||||
}, { status: 500 });
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import loadEnv from "../handler/load-env";
|
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
import loadEnv from '../handler/load-env';
|
||||||
import syncConnectedOnline from "../handler/sync-connected-online";
|
import syncConnectedOnline from '../handler/sync-connected-online';
|
||||||
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|
||||||
export const POST = async (request: NextRequest) => {
|
export const POST = async (request: NextRequest) => {
|
||||||
loadEnv();
|
loadEnv();
|
||||||
@@ -14,14 +15,20 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await syncConnectedOnline();
|
await syncConnectedOnline();
|
||||||
console.info("Successfully synced connected-online");
|
console.info('Successfully synced connected-online');
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Successfully synced connected-online',
|
message: 'Successfully synced connected-online',
|
||||||
}, { status: 200 });
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error syncing connected-online", e);
|
console.error('Error syncing connected-online', e);
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
message: 'Failed to sync connected-online',
|
message: 'Failed to sync connected-online',
|
||||||
}, { status: 500 });
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
|
|
||||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
|
import { retrieveOrder } from '@lib/data';
|
||||||
import { retrieveOrder } from "@lib/data";
|
|
||||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
import { getAccountAdmin } from '~/lib/services/account.service';
|
||||||
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
|
import {
|
||||||
import loadEnv from "../handler/load-env";
|
composeOrderTestResponseXML,
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
sendPrivateMessageTestResponse,
|
||||||
|
} from '~/lib/services/medipost/medipostTest.service';
|
||||||
|
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||||
|
import { getAnalysisOrdersAdmin } from '~/lib/services/order.service';
|
||||||
|
|
||||||
|
import loadEnv from '../handler/load-env';
|
||||||
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
loadEnv();
|
loadEnv();
|
||||||
@@ -16,17 +22,27 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
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) {
|
for (const medreportOrder of analysisOrders) {
|
||||||
const medusaOrderId = medreportOrder.medusa_order_id;
|
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 account = await getAccountAdmin({
|
||||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
|
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 idsToSend = orderedAnalysisElementsIds;
|
||||||
const messageXml = await composeOrderTestResponseXML({
|
const messageXml = await composeOrderTestResponseXML({
|
||||||
person: {
|
person: {
|
||||||
@@ -35,8 +51,12 @@ export async function POST(request: NextRequest) {
|
|||||||
lastName: account.last_name ?? '',
|
lastName: account.last_name ?? '',
|
||||||
phone: account.phone ?? '',
|
phone: account.phone ?? '',
|
||||||
},
|
},
|
||||||
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
orderedAnalysisElementsIds: idsToSend
|
||||||
orderedAnalysesIds: idsToSend.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
.map(({ analysisElementId }) => analysisElementId)
|
||||||
|
.filter(Boolean) as number[],
|
||||||
|
orderedAnalysesIds: idsToSend
|
||||||
|
.map(({ analysisId }) => analysisId)
|
||||||
|
.filter(Boolean) as number[],
|
||||||
orderId: medreportOrder.id,
|
orderId: medreportOrder.id,
|
||||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||||
});
|
});
|
||||||
@@ -44,7 +64,7 @@ export async function POST(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
await sendPrivateMessageTestResponse({ messageXml });
|
await sendPrivateMessageTestResponse({ messageXml });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending private message test response: ", error);
|
console.error('Error sending private message test response: ', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from 'next/server';
|
||||||
import { getAnalysisOrder } from "~/lib/services/order.service";
|
|
||||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
|
import { retrieveOrder } from '@lib/data';
|
||||||
import { retrieveOrder } from "@lib/data";
|
|
||||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
import { getAccountAdmin } from '~/lib/services/account.service';
|
||||||
import { upsertMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service";
|
import { upsertMedipostActionLog } from '~/lib/services/medipost/medipostMessageBase.service';
|
||||||
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
|
import {
|
||||||
|
composeOrderTestResponseXML,
|
||||||
|
sendPrivateMessageTestResponse,
|
||||||
|
} from '~/lib/services/medipost/medipostTest.service';
|
||||||
|
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
|
||||||
|
import { getAnalysisOrder } from '~/lib/services/order.service';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// const isDev = process.env.NODE_ENV === 'development';
|
// const isDev = process.env.NODE_ENV === 'development';
|
||||||
@@ -14,13 +19,19 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const { medusaOrderId } = await request.json();
|
const { medusaOrderId } = await request.json();
|
||||||
|
|
||||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
const medusaOrder = await retrieveOrder(medusaOrderId);
|
||||||
const analysisOrder = await getAnalysisOrder({ medusaOrderId });
|
const analysisOrder = await getAnalysisOrder({ medusaOrderId });
|
||||||
|
|
||||||
const account = await getAccountAdmin({ primaryOwnerUserId: analysisOrder.user_id });
|
const account = await getAccountAdmin({
|
||||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
|
primaryOwnerUserId: analysisOrder.user_id,
|
||||||
|
});
|
||||||
|
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({
|
||||||
|
medusaOrder,
|
||||||
|
});
|
||||||
|
|
||||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
console.info(
|
||||||
|
`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`,
|
||||||
|
);
|
||||||
const messageXml = await composeOrderTestResponseXML({
|
const messageXml = await composeOrderTestResponseXML({
|
||||||
person: {
|
person: {
|
||||||
idCode: account.personal_code!,
|
idCode: account.personal_code!,
|
||||||
@@ -28,8 +39,12 @@ export async function POST(request: Request) {
|
|||||||
lastName: account.last_name ?? '',
|
lastName: account.last_name ?? '',
|
||||||
phone: account.phone ?? '',
|
phone: account.phone ?? '',
|
||||||
},
|
},
|
||||||
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
orderedAnalysisElementsIds: orderedAnalysisElementsIds
|
||||||
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
.map(({ analysisElementId }) => analysisElementId)
|
||||||
|
.filter(Boolean) as number[],
|
||||||
|
orderedAnalysesIds: orderedAnalysisElementsIds
|
||||||
|
.map(({ analysisId }) => analysisId)
|
||||||
|
.filter(Boolean) as number[],
|
||||||
orderId: analysisOrder.id,
|
orderId: analysisOrder.id,
|
||||||
orderCreatedAt: new Date(analysisOrder.created_at),
|
orderCreatedAt: new Date(analysisOrder.created_at),
|
||||||
});
|
});
|
||||||
@@ -42,7 +57,7 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
await sendPrivateMessageTestResponse({ messageXml });
|
await sendPrivateMessageTestResponse({ messageXml });
|
||||||
} catch (error) {
|
} 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 });
|
return NextResponse.json({ success: true });
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ import Link from 'next/link';
|
|||||||
import type { AuthError } from '@supabase/supabase-js';
|
import type { AuthError } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { ResendAuthLinkForm } from '@kit/auth/resend-email-link';
|
import { ResendAuthLinkForm } from '@kit/auth/resend-email-link';
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
interface AuthCallbackErrorPageProps {
|
interface AuthCallbackErrorPageProps {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
error: string;
|
error: string;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
|
|
||||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
import { createAuthCallbackService, getErrorURLParameters } from '@kit/supabase/auth';
|
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
import {
|
||||||
|
createAuthCallbackService,
|
||||||
|
getErrorURLParameters,
|
||||||
|
} from '@kit/supabase/auth';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
const ERROR_PATH = '/auth/callback/error';
|
const ERROR_PATH = '/auth/callback/error';
|
||||||
|
|
||||||
const redirectOnError = (searchParams?: string) => {
|
const redirectOnError = (searchParams?: string) => {
|
||||||
return redirect(`${ERROR_PATH}${searchParams ? `?${searchParams}` : ''}`);
|
return redirect(`${ERROR_PATH}${searchParams ? `?${searchParams}` : ''}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@@ -44,15 +47,13 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const service = createAuthCallbackService(getSupabaseServerClient());
|
const service = createAuthCallbackService(getSupabaseServerClient());
|
||||||
const oauthResult = await service.exchangeCodeForSession(authCode);
|
const oauthResult = await service.exchangeCodeForSession(authCode);
|
||||||
if (!("isSuccess" in oauthResult)) {
|
if (!('isSuccess' in oauthResult)) {
|
||||||
return redirectOnError(oauthResult.searchParams);
|
return redirectOnError(oauthResult.searchParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = createAccountsApi(getSupabaseServerClient());
|
const api = createAccountsApi(getSupabaseServerClient());
|
||||||
|
|
||||||
const account = await api.getPersonalAccountByUserId(
|
const account = await api.getPersonalAccountByUserId(oauthResult.user.id);
|
||||||
oauthResult.user.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!account.email || !account.name || !account.last_name) {
|
if (!account.email || !account.name || !account.last_name) {
|
||||||
return redirect(pathsConfig.auth.updateAccount);
|
return redirect(pathsConfig.auth.updateAccount);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
|
|
||||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { createAuthCallbackService } from '@kit/supabase/auth';
|
import { createAuthCallbackService } from '@kit/supabase/auth';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const service = createAuthCallbackService(getSupabaseServerClient());
|
const service = createAuthCallbackService(getSupabaseServerClient());
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
||||||
import { SuccessNotification } from '@kit/notifications/components';
|
import { SuccessNotification } from '@kit/notifications/components';
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
|
||||||
const MembershipConfirmationNotification: React.FC<{
|
const MembershipConfirmationNotification: React.FC<{
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
async function SiteLayout(props: React.PropsWithChildren) {
|
async function SiteLayout(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
|
|
||||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { PasswordResetRequestContainer } from '@kit/auth/password-reset';
|
import { PasswordResetRequestContainer } from '@kit/auth/password-reset';
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Heading } from '@kit/ui/heading';
|
import { Heading } from '@kit/ui/heading';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Loading from '@/app/home/loading';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { getSupabaseBrowserClient } from '@/packages/supabase/src/clients/browser-client';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import Loading from '@/app/home/loading';
|
||||||
|
import { getSupabaseBrowserClient } from '@/packages/supabase/src/clients/browser-client';
|
||||||
|
|
||||||
export function SignInPageClientRedirect() {
|
export function SignInPageClientRedirect() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function signIn() {
|
async function signIn() {
|
||||||
const { data, error } = await getSupabaseBrowserClient()
|
const { data, error } =
|
||||||
.auth
|
await getSupabaseBrowserClient().auth.signInWithOAuth({
|
||||||
.signInWithOAuth({
|
|
||||||
provider: 'keycloak',
|
provider: 'keycloak',
|
||||||
options: {
|
options: {
|
||||||
redirectTo: `${window.location.origin}/auth/callback`,
|
redirectTo: `${window.location.origin}/auth/callback`,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
prompt: 'login',
|
prompt: 'login',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
|
|
||||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { getServerAuthConfig, pathsConfig } from '@kit/shared/config';
|
|||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import { SignInPageClientRedirect } from './components/SignInPageClientRedirect';
|
|
||||||
import PasswordOption from './components/PasswordOption';
|
import PasswordOption from './components/PasswordOption';
|
||||||
|
import { SignInPageClientRedirect } from './components/SignInPageClientRedirect';
|
||||||
|
|
||||||
interface SignInPageProps {
|
interface SignInPageProps {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
|
|
||||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Heading } from '@kit/ui/heading';
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import Isikukood from 'isikukood';
|
import Isikukood from 'isikukood';
|
||||||
import parsePhoneNumber from 'libphonenumber-js/min';
|
import parsePhoneNumber from 'libphonenumber-js/min';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const updateAccountSchema = {
|
const updateAccountSchema = {
|
||||||
firstName: z
|
firstName: z
|
||||||
@@ -39,14 +39,18 @@ const updateAccountSchema = {
|
|||||||
(phone) => {
|
(phone) => {
|
||||||
try {
|
try {
|
||||||
const phoneNumber = parsePhoneNumber(phone);
|
const phoneNumber = parsePhoneNumber(phone);
|
||||||
return !!phoneNumber && phoneNumber.isValid() && phoneNumber.country === 'EE';
|
return (
|
||||||
|
!!phoneNumber &&
|
||||||
|
phoneNumber.isValid() &&
|
||||||
|
phoneNumber.country === 'EE'
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'common:formFieldError.invalidPhoneNumber',
|
message: 'common:formFieldError.invalidPhoneNumber',
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
weight: z
|
weight: z
|
||||||
@@ -81,7 +85,12 @@ export const UpdateAccountSchemaServer = z.object({
|
|||||||
height: updateAccountSchema.height.nullable(),
|
height: updateAccountSchema.height.nullable(),
|
||||||
userConsent: updateAccountSchema.userConsent,
|
userConsent: updateAccountSchema.userConsent,
|
||||||
});
|
});
|
||||||
export const UpdateAccountSchemaClient = ({ isEmailUser }: { isEmailUser: boolean }) => z.object({
|
export const UpdateAccountSchemaClient = ({
|
||||||
|
isEmailUser,
|
||||||
|
}: {
|
||||||
|
isEmailUser: boolean;
|
||||||
|
}) =>
|
||||||
|
z.object({
|
||||||
firstName: updateAccountSchema.firstName,
|
firstName: updateAccountSchema.firstName,
|
||||||
lastName: updateAccountSchema.lastName,
|
lastName: updateAccountSchema.lastName,
|
||||||
personalCode: updateAccountSchema.personalCode,
|
personalCode: updateAccountSchema.personalCode,
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import { updateCustomer } from '@lib/data/customer';
|
|||||||
|
|
||||||
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
|
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { UpdateAccountSchemaServer } from '../schemas/update-account.schema';
|
import { UpdateAccountSchemaServer } from '../schemas/update-account.schema';
|
||||||
|
|
||||||
@@ -32,14 +31,14 @@ export const onUpdateAccount = enhanceAction(
|
|||||||
phone: params.phone,
|
phone: params.phone,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to update Medusa customer", e);
|
console.error('Failed to update Medusa customer', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUnseenMembershipConfirmation =
|
const hasUnseenMembershipConfirmation =
|
||||||
await api.hasUnseenMembershipConfirmation();
|
await api.hasUnseenMembershipConfirmation();
|
||||||
return {
|
return {
|
||||||
hasUnseenMembershipConfirmation,
|
hasUnseenMembershipConfirmation,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
schema: UpdateAccountSchemaServer,
|
schema: UpdateAccountSchemaServer,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
async function SiteLayout(props: React.PropsWithChildren) {
|
async function SiteLayout(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
|
|
||||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { MultiFactorChallengeContainer } from '@kit/auth/mfa';
|
import { MultiFactorChallengeContainer } from '@kit/auth/mfa';
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
@@ -41,7 +40,8 @@ async function VerifyPage(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextPath = (await props.searchParams).next;
|
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 (
|
return (
|
||||||
<MultiFactorChallengeContainer
|
<MultiFactorChallengeContainer
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const AnalysisDoctor = ({
|
|||||||
/>
|
/>
|
||||||
{endIcon || <div className="mx-2 w-4" />}
|
{endIcon || <div className="mx-2 w-4" />}
|
||||||
</>
|
</>
|
||||||
) : (isCancelled ? null : (
|
) : isCancelled ? null : (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-3 sm:ml-auto">
|
<div className="flex items-center gap-3 sm:ml-auto">
|
||||||
<div className="font-semibold">
|
<div className="font-semibold">
|
||||||
@@ -140,7 +140,7 @@ const AnalysisDoctor = ({
|
|||||||
<div className="mx-8 w-[60px]"></div>
|
<div className="mx-8 w-[60px]"></div>
|
||||||
<AnalysisLevelBarSkeleton />
|
<AnalysisLevelBarSkeleton />
|
||||||
</>
|
</>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
|
|||||||
import { ArrowDown } from 'lucide-react';
|
import { ArrowDown } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { AnalysisResultForDisplay } from './analysis-doctor';
|
import { AnalysisResultForDisplay } from './analysis-doctor';
|
||||||
|
|
||||||
export enum AnalysisResultLevel {
|
export enum AnalysisResultLevel {
|
||||||
@@ -48,7 +49,7 @@ const Level = ({
|
|||||||
|
|
||||||
export const AnalysisLevelBarSkeleton = () => {
|
export const AnalysisLevelBarSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||||
<Level color="gray-200" />
|
<Level color="gray-200" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -65,8 +66,11 @@ const AnalysisLevelBar = ({
|
|||||||
level: AnalysisResultLevel;
|
level: AnalysisResultLevel;
|
||||||
results: AnalysisResultForDisplay;
|
results: AnalysisResultForDisplay;
|
||||||
}) => {
|
}) => {
|
||||||
|
const {
|
||||||
const { norm_lower: lower, norm_upper: upper, response_value: value } = results;
|
norm_lower: lower,
|
||||||
|
norm_upper: upper,
|
||||||
|
response_value: value,
|
||||||
|
} = results;
|
||||||
const arrowLocation = useMemo(() => {
|
const arrowLocation = useMemo(() => {
|
||||||
if (value < lower!) {
|
if (value < lower!) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -85,24 +89,23 @@ const AnalysisLevelBar = ({
|
|||||||
return calculated;
|
return calculated;
|
||||||
}, [value, upper, lower]);
|
}, [value, upper, lower]);
|
||||||
|
|
||||||
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [
|
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(
|
||||||
|
() => [
|
||||||
level === AnalysisResultLevel.VERY_LOW,
|
level === AnalysisResultLevel.VERY_LOW,
|
||||||
level === AnalysisResultLevel.LOW,
|
level === AnalysisResultLevel.LOW,
|
||||||
level === AnalysisResultLevel.HIGH,
|
level === AnalysisResultLevel.HIGH,
|
||||||
level === AnalysisResultLevel.VERY_HIGH,
|
level === AnalysisResultLevel.VERY_HIGH,
|
||||||
], [level, value, upper, lower]);
|
],
|
||||||
|
[level, value, upper, lower],
|
||||||
|
);
|
||||||
|
|
||||||
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-[60%] sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
<div className="mt-4 flex h-3 w-[60%] max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||||
{normLowerIncluded && (
|
{normLowerIncluded && (
|
||||||
<>
|
<>
|
||||||
<Level
|
<Level isActive={isVeryLow} color="destructive" isFirst />
|
||||||
isActive={isVeryLow}
|
|
||||||
color="destructive"
|
|
||||||
isFirst
|
|
||||||
/>
|
|
||||||
<Level isActive={isLow} color="warning" />
|
<Level isActive={isLow} color="warning" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -110,21 +113,16 @@ const AnalysisLevelBar = ({
|
|||||||
<Level
|
<Level
|
||||||
isFirst={!normLowerIncluded}
|
isFirst={!normLowerIncluded}
|
||||||
isLast={!normUpperIncluded}
|
isLast={!normUpperIncluded}
|
||||||
{...(hasAbnormalLevel ? { color: "warning", isActive: false } : { color: "success", isActive: true })}
|
{...(hasAbnormalLevel
|
||||||
|
? { color: 'warning', isActive: false }
|
||||||
|
: { color: 'success', isActive: true })}
|
||||||
arrowLocation={arrowLocation}
|
arrowLocation={arrowLocation}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{normUpperIncluded && (
|
{normUpperIncluded && (
|
||||||
<>
|
<>
|
||||||
<Level
|
<Level isActive={isHigh} color="warning" />
|
||||||
isActive={isHigh}
|
<Level isActive={isVeryHigh} color="destructive" isLast />
|
||||||
color="warning"
|
|
||||||
/>
|
|
||||||
<Level
|
|
||||||
isActive={isVeryHigh}
|
|
||||||
color="destructive"
|
|
||||||
isLast
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ import {
|
|||||||
doctorAnalysisFeedbackFormSchema,
|
doctorAnalysisFeedbackFormSchema,
|
||||||
} from '@kit/doctor/schema/doctor-analysis.schema';
|
} from '@kit/doctor/schema/doctor-analysis.schema';
|
||||||
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
||||||
import {
|
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
|
||||||
useCurrentLocaleLanguageNames
|
|
||||||
} from '@kit/shared/hooks';
|
|
||||||
import { getFullName } from '@kit/shared/utils';
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getUserDoneResponsesAction,
|
getUserDoneResponsesAction,
|
||||||
getUserInProgressResponsesAction,
|
getUserInProgressResponsesAction,
|
||||||
} from '@kit/doctor/actions/table-data-fetching-actions';
|
} from '@kit/doctor/actions/table-data-fetching-actions';
|
||||||
|
|
||||||
import ResultsTableWrapper from './results-table-wrapper';
|
import ResultsTableWrapper from './results-table-wrapper';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|
||||||
import { isDoctor } from '@kit/doctor/lib/server/utils/is-doctor';
|
import { isDoctor } from '@kit/doctor/lib/server/utils/is-doctor';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTransition } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { capitalize } from 'lodash';
|
||||||
import { Eye } from 'lucide-react';
|
import { Eye } from 'lucide-react';
|
||||||
|
|
||||||
import { getResultSetName } from '@kit/doctor/lib/helpers';
|
import { getResultSetName } from '@kit/doctor/lib/helpers';
|
||||||
@@ -26,7 +27,6 @@ import {
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import DoctorJobSelect from './doctor-job-select';
|
import DoctorJobSelect from './doctor-job-select';
|
||||||
import { capitalize } from 'lodash';
|
|
||||||
|
|
||||||
export default function ResultsTable({
|
export default function ResultsTable({
|
||||||
results = [],
|
results = [],
|
||||||
@@ -189,7 +189,9 @@ export default function ResultsTable({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{capitalize(
|
{capitalize(
|
||||||
languageNames.of(result?.patient?.preferred_locale ?? 'et'),
|
languageNames.of(
|
||||||
|
result?.patient?.preferred_locale ?? 'et',
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -50,5 +50,3 @@ async function AnalysisPage({
|
|||||||
|
|
||||||
export default DoctorGuard(AnalysisPage);
|
export default DoctorGuard(AnalysisPage);
|
||||||
const loadResult = cache(getAnalysisResultsForDoctor);
|
const loadResult = cache(getAnalysisResultsForDoctor);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { cookies } from 'next/headers';
|
|||||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
|
|
||||||
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
|
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
|
||||||
import { DoctorSidebar } from './_components/doctor-sidebar';
|
import { DoctorSidebar } from './_components/doctor-sidebar';
|
||||||
import { DoctorMobileNavigation } from './_components/mobile-navigation';
|
import { DoctorMobileNavigation } from './_components/mobile-navigation';
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import Link from 'next/link';
|
|||||||
import { ArrowLeft, MessageCircle } from 'lucide-react';
|
import { ArrowLeft, MessageCircle } from 'lucide-react';
|
||||||
|
|
||||||
import { useCaptureException } from '@kit/monitoring/hooks';
|
import { useCaptureException } from '@kit/monitoring/hooks';
|
||||||
|
import { RootProviders } from '@kit/shared/components/root-providers';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Heading } from '@kit/ui/heading';
|
import { Heading } from '@kit/ui/heading';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||||
import { RootProviders } from '@kit/shared/components/root-providers';
|
|
||||||
|
|
||||||
const GlobalErrorPage = ({
|
const GlobalErrorPage = ({
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ export default async function AnalysisResultsPage({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!account?.id) {
|
if (!account?.id) {
|
||||||
return redirect("/");
|
return redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
await createPageViewLog({
|
await createPageViewLog({
|
||||||
@@ -47,27 +48,31 @@ export default async function AnalysisResultsPage({
|
|||||||
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||||
description={<Trans i18nKey="analysis-results:descriptionEmpty" />}
|
description={<Trans i18nKey="analysis-results:descriptionEmpty" />}
|
||||||
/>
|
/>
|
||||||
<PageBody className="gap-4">
|
<PageBody className="gap-4"></PageBody>
|
||||||
</PageBody>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
|
const orderedAnalysisElements = analysisResponse.orderedAnalysisElements;
|
||||||
const hasOrderedAnalysisElements = orderedAnalysisElements.length > 0;
|
const hasOrderedAnalysisElements = orderedAnalysisElements.length > 0;
|
||||||
const isPartialStatus = analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
|
const isPartialStatus =
|
||||||
|
analysisResponse.order.status === 'PARTIAL_ANALYSIS_RESPONSE';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||||
description={hasOrderedAnalysisElements ? (
|
description={
|
||||||
isPartialStatus
|
hasOrderedAnalysisElements ? (
|
||||||
? <Trans i18nKey="analysis-results:descriptionPartial" />
|
isPartialStatus ? (
|
||||||
: <Trans i18nKey="analysis-results:description" />
|
<Trans i18nKey="analysis-results:descriptionPartial" />
|
||||||
|
) : (
|
||||||
|
<Trans i18nKey="analysis-results:description" />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
<Trans i18nKey="analysis-results:descriptionEmpty" />
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
@@ -106,13 +111,15 @@ export default async function AnalysisResultsPage({
|
|||||||
orderedAnalysisElements.map((element, index) => (
|
orderedAnalysisElements.map((element, index) => (
|
||||||
<React.Fragment key={element.analysisIdOriginal}>
|
<React.Fragment key={element.analysisIdOriginal}>
|
||||||
<Analysis element={element} />
|
<Analysis element={element} />
|
||||||
{element.results?.nestedElements?.map((nestedElement, nestedIndex) => (
|
{element.results?.nestedElements?.map(
|
||||||
|
(nestedElement, nestedIndex) => (
|
||||||
<Analysis
|
<Analysis
|
||||||
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
key={`nested-${nestedElement.analysisElementOriginalId}-${nestedIndex}`}
|
||||||
nestedElement={nestedElement}
|
nestedElement={nestedElement}
|
||||||
isNestedElement
|
isNestedElement
|
||||||
/>
|
/>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useMemo } from 'react';
|
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 { ArrowDown } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
import type { AnalysisResultDetailsElementResults } from '@/packages/features/user-analyses/src/types/analysis-results';
|
|
||||||
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
|
||||||
|
|
||||||
type AnalysisResultLevelBarResults = Pick<AnalysisResultDetailsElementResults, 'normLower' | 'normUpper' | 'responseValue'>;
|
type AnalysisResultLevelBarResults = Pick<
|
||||||
|
AnalysisResultDetailsElementResults,
|
||||||
|
'normLower' | 'normUpper' | 'responseValue'
|
||||||
|
>;
|
||||||
|
|
||||||
const Level = ({
|
const Level = ({
|
||||||
isActive = false,
|
isActive = false,
|
||||||
@@ -34,22 +37,29 @@ const Level = ({
|
|||||||
{isActive && (
|
{isActive && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
|
||||||
{...(arrowLocation ? {
|
{...(arrowLocation
|
||||||
|
? {
|
||||||
style: {
|
style: {
|
||||||
left: `${arrowLocation}%`,
|
left: `${arrowLocation}%`,
|
||||||
...(arrowLocation > 92.5 && { left: '92.5%' }),
|
...(arrowLocation > 92.5 && { left: '92.5%' }),
|
||||||
...(arrowLocation < 7.5 && { left: '7.5%' }),
|
...(arrowLocation < 7.5 && { left: '7.5%' }),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} : {})}
|
: {})}
|
||||||
>
|
>
|
||||||
<ArrowDown strokeWidth={2} />
|
<ArrowDown strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{color === 'success' && typeof normRangeText === 'string' && (
|
{color === 'success' && typeof normRangeText === 'string' && (
|
||||||
<p className={cn("absolute bottom-[-18px] left-3/8 text-xs text-muted-foreground font-bold whitespace-nowrap", {
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground absolute bottom-[-18px] left-3/8 text-xs font-bold whitespace-nowrap',
|
||||||
|
{
|
||||||
'opacity-60': isActive,
|
'opacity-60': isActive,
|
||||||
})}>
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
{normRangeText}
|
{normRangeText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -59,11 +69,7 @@ const Level = ({
|
|||||||
|
|
||||||
const AnalysisLevelBar = ({
|
const AnalysisLevelBar = ({
|
||||||
level,
|
level,
|
||||||
results: {
|
results: { normLower: lower, normUpper: upper, responseValue: value },
|
||||||
normLower: lower,
|
|
||||||
normUpper: upper,
|
|
||||||
responseValue: value,
|
|
||||||
},
|
|
||||||
normRangeText,
|
normRangeText,
|
||||||
}: {
|
}: {
|
||||||
level: AnalysisResultLevel;
|
level: AnalysisResultLevel;
|
||||||
@@ -127,7 +133,7 @@ const AnalysisLevelBar = ({
|
|||||||
// If pending results, show gray bar
|
// If pending results, show gray bar
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex h-3 w-60% sm:w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
<div className="w-60% mt-4 flex h-3 max-w-[360px] gap-1 sm:mt-0 sm:w-[35%]">
|
||||||
<Level color="gray-200" isFirst isLast />
|
<Level color="gray-200" isFirst isLast />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -146,29 +152,25 @@ const AnalysisLevelBar = ({
|
|||||||
const [warning, normal, critical] = [
|
const [warning, normal, critical] = [
|
||||||
{
|
{
|
||||||
isActive: isWarning,
|
isActive: isWarning,
|
||||||
color: "warning",
|
color: 'warning',
|
||||||
...(isWarning ? { arrowLocation } : {}),
|
...(isWarning ? { arrowLocation } : {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isActive: isNormal,
|
isActive: isNormal,
|
||||||
color: "success",
|
color: 'success',
|
||||||
normRangeText,
|
normRangeText,
|
||||||
...(isNormal ? { arrowLocation } : {}),
|
...(isNormal ? { arrowLocation } : {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isActive: isCritical,
|
isActive: isCritical,
|
||||||
color: "destructive",
|
color: 'destructive',
|
||||||
isLast: true,
|
isLast: true,
|
||||||
...(isCritical ? { arrowLocation } : {}),
|
...(isCritical ? { arrowLocation } : {}),
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
if (!hasLowerBound) {
|
if (!hasLowerBound) {
|
||||||
return [
|
return [{ ...normal, isFirst: true }, warning, critical] as const;
|
||||||
{ ...normal, isFirst: true },
|
|
||||||
warning,
|
|
||||||
critical,
|
|
||||||
] as const;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -176,16 +178,27 @@ const AnalysisLevelBar = ({
|
|||||||
normal,
|
normal,
|
||||||
{ ...critical, isLast: true },
|
{ ...critical, isLast: true },
|
||||||
] as const;
|
] as const;
|
||||||
}, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]);
|
}, [
|
||||||
|
isValueBelowLower,
|
||||||
|
isValueAboveUpper,
|
||||||
|
isValueInNormalRange,
|
||||||
|
arrowLocation,
|
||||||
|
normRangeText,
|
||||||
|
isNormal,
|
||||||
|
isWarning,
|
||||||
|
isCritical,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div
|
||||||
"flex h-3 gap-1",
|
className={cn(
|
||||||
"mt-4 sm:mt-0",
|
'flex h-3 gap-1',
|
||||||
"w-[60%] sm:w-[35%]",
|
'mt-4 sm:mt-0',
|
||||||
"min-w-[50vw] sm:min-w-auto",
|
'w-[60%] sm:w-[35%]',
|
||||||
"max-w-[360px]",
|
'min-w-[50vw] sm:min-w-auto',
|
||||||
)}>
|
'max-w-[360px]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Level {...first} />
|
<Level {...first} />
|
||||||
<Level {...second} />
|
<Level {...second} />
|
||||||
<Level {...third} />
|
<Level {...third} />
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Info } from 'lucide-react';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AnalysisResultDetailsElement,
|
AnalysisResultDetailsElement,
|
||||||
AnalysisResultsDetailsElementNested,
|
AnalysisResultsDetailsElementNested,
|
||||||
} from '@/packages/features/user-analyses/src/types/analysis-results';
|
} from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||||
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
import { AnalysisResultLevel } from '@/packages/features/user-analyses/src/types/analysis-results';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
@@ -64,7 +65,11 @@ const Analysis = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { responseValue, responseValueIsNegative, responseValueIsWithinNorm } = results;
|
const {
|
||||||
|
responseValue,
|
||||||
|
responseValueIsNegative,
|
||||||
|
responseValueIsWithinNorm,
|
||||||
|
} = results;
|
||||||
if (responseValue === null || responseValue === undefined) {
|
if (responseValue === null || responseValue === undefined) {
|
||||||
if (hasIsNegative) {
|
if (hasIsNegative) {
|
||||||
if (responseValueIsNegative) {
|
if (responseValueIsNegative) {
|
||||||
@@ -107,7 +112,8 @@ const Analysis = ({
|
|||||||
|
|
||||||
const isCancelled = Number(results?.status) === 5;
|
const isCancelled = Number(results?.status) === 5;
|
||||||
const nestedElements = results?.nestedElements ?? null;
|
const nestedElements = results?.nestedElements ?? null;
|
||||||
const hasNestedElements = Array.isArray(nestedElements) && nestedElements.length > 0;
|
const hasNestedElements =
|
||||||
|
Array.isArray(nestedElements) && nestedElements.length > 0;
|
||||||
|
|
||||||
const normRangeText = (() => {
|
const normRangeText = (() => {
|
||||||
if (normLower === null && normUpper === null) {
|
if (normLower === null && normUpper === null) {
|
||||||
@@ -118,9 +124,17 @@ const Analysis = ({
|
|||||||
const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
|
const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("border-border rounded-lg border px-5", { 'ml-8': isNestedElement })}>
|
<div
|
||||||
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:py-3 sm:h-[65px] sm:flex-row sm:gap-0">
|
className={cn('border-border rounded-lg border px-5', {
|
||||||
<div className={cn("flex items-center gap-2 font-semibold", { 'font-bold': isNestedElement })}>
|
'ml-8': isNestedElement,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-between gap-2 pt-3 pb-6 sm:h-[65px] sm:flex-row sm:gap-0 sm:py-3">
|
||||||
|
<div
|
||||||
|
className={cn('flex items-center gap-2 font-semibold', {
|
||||||
|
'font-bold': isNestedElement,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
{results?.responseTime && (
|
{results?.responseTime && (
|
||||||
<div
|
<div
|
||||||
@@ -147,7 +161,7 @@ const Analysis = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCancelled && (
|
{isCancelled && (
|
||||||
<div className="text-red-600 font-semibold text-sm">
|
<div className="text-sm font-semibold text-red-600">
|
||||||
<Trans i18nKey="analysis-results:cancelled" />
|
<Trans i18nKey="analysis-results:cancelled" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -157,9 +171,15 @@ const Analysis = ({
|
|||||||
<div className="flex items-center gap-3 sm:ml-auto">
|
<div className="flex items-center gap-3 sm:ml-auto">
|
||||||
<div
|
<div
|
||||||
className={cn('font-semibold', {
|
className={cn('font-semibold', {
|
||||||
'text-yellow-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.WARNING,
|
'text-yellow-600':
|
||||||
'text-red-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.CRITICAL,
|
hasTextualResponse &&
|
||||||
'text-green-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.NORMAL,
|
analysisResultLevel === AnalysisResultLevel.WARNING,
|
||||||
|
'text-red-600':
|
||||||
|
hasTextualResponse &&
|
||||||
|
analysisResultLevel === AnalysisResultLevel.CRITICAL,
|
||||||
|
'text-green-600':
|
||||||
|
hasTextualResponse &&
|
||||||
|
analysisResultLevel === AnalysisResultLevel.NORMAL,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import Modal from '@modules/common/components/modal';
|
||||||
|
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
import { Button } from '@kit/ui/shadcn/button';
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
import Modal from "@modules/common/components/modal"
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import Analysis from '../_components/analysis';
|
import Analysis from '../_components/analysis';
|
||||||
import { analysisResponses } from './test-responses';
|
import { analysisResponses } from './test-responses';
|
||||||
@@ -19,16 +20,15 @@ export default function AnalysisResultsPage() {
|
|||||||
<PageBody className="gap-4">
|
<PageBody className="gap-4">
|
||||||
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
|
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
|
||||||
<div>
|
<div>
|
||||||
<h2>
|
<h2>Analüüsi tulemused demo</h2>
|
||||||
Analüüsi tulemused demo
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{analysisResponses.map(({ id, orderedAnalysisElements }, index) => {
|
{analysisResponses.map(({ id, orderedAnalysisElements }, index) => {
|
||||||
const isOpen = openBlocks.includes(id);
|
const isOpen = openBlocks.includes(id);
|
||||||
const closeModal = () => setOpenBlocks(openBlocks.filter((block) => block !== id));
|
const closeModal = () =>
|
||||||
|
setOpenBlocks(openBlocks.filter((block) => block !== id));
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex flex-col gap-2 py-4">
|
<div key={index} className="flex flex-col gap-2 py-4">
|
||||||
<div className="flex flex-col gap-2 pb-4">
|
<div className="flex flex-col gap-2 pb-4">
|
||||||
@@ -52,12 +52,21 @@ export default function AnalysisResultsPage() {
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Modal isOpen={isOpen} close={closeModal} size="large">
|
<Modal isOpen={isOpen} close={closeModal} size="large">
|
||||||
<div className="overflow-y-auto">
|
<div className="overflow-y-auto">
|
||||||
|
|
||||||
<p>NormiStaatus</p>
|
<p>NormiStaatus</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>0 - testi väärtus jääb normaalväärtuste piirkonda või on määramata,</li>
|
<li>
|
||||||
<li>1 - testi väärtus jääb hoiatava (tähelepanu suunava) märkega piirkonda,</li>
|
0 - testi väärtus jääb normaalväärtuste piirkonda
|
||||||
<li>2 - testi väärtus on normaalväärtuste piirkonnast väljas või kõrgendatud tähelepanu nõudvas piirkonnas.</li>
|
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>
|
</ul>
|
||||||
|
|
||||||
<p>UuringOlek</p>
|
<p>UuringOlek</p>
|
||||||
@@ -70,7 +79,7 @@ export default function AnalysisResultsPage() {
|
|||||||
<li>6 - Tühistatud,</li>
|
<li>6 - Tühistatud,</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<pre className="text-sm bg-muted p-4 rounded-md">
|
<pre className="bg-muted rounded-md p-4 text-sm">
|
||||||
{JSON.stringify(orderedAnalysisElements, null, 2)}
|
{JSON.stringify(orderedAnalysisElements, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +99,7 @@ export default function AnalysisResultsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,13 @@ import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs';
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import BookingContainer from '~/home/(user)/_components/booking/booking-container';
|
import BookingContainer from '~/home/(user)/_components/booking/booking-container';
|
||||||
|
import { loadCurrentUserAccount } from '~/home/(user)/_lib/server/load-user-account';
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import { PageViewAction, createPageViewLog } from '~/lib/services/audit/pageView.service';
|
import {
|
||||||
import { loadCurrentUserAccount } from '~/home/(user)/_lib/server/load-user-account';
|
PageViewAction,
|
||||||
|
createPageViewLog,
|
||||||
|
} from '~/lib/services/audit/pageView.service';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { MontonioOrderToken } from '@/app/home/(user)/_components/cart/types';
|
||||||
|
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||||
|
import { placeOrder, retrieveCart } from '@lib/data/cart';
|
||||||
|
import { listProductTypes } from '@lib/data/products';
|
||||||
|
import type { StoreOrder } from '@medusajs/types';
|
||||||
import { MontonioOrderToken } from '@/app/home/(user)/_components/cart/types';
|
import { MontonioOrderToken } from '@/app/home/(user)/_components/cart/types';
|
||||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||||
import { placeOrder, retrieveCart } from '@lib/data/cart';
|
import { placeOrder, retrieveCart } from '@lib/data/cart';
|
||||||
@@ -24,6 +29,20 @@ const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
|||||||
const ANALYSIS_TYPE_HANDLE = 'synlab-analysis';
|
const ANALYSIS_TYPE_HANDLE = 'synlab-analysis';
|
||||||
const MONTONIO_PAID_STATUS = 'PAID';
|
const MONTONIO_PAID_STATUS = 'PAID';
|
||||||
|
|
||||||
|
const env = () =>
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
emailSender: z
|
||||||
|
.string({
|
||||||
|
error: 'EMAIL_SENDER is required',
|
||||||
|
})
|
||||||
|
.min(1),
|
||||||
|
siteUrl: z
|
||||||
|
.string({
|
||||||
|
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||||
|
})
|
||||||
|
.min(1),
|
||||||
|
isEnabledDispatchOnMontonioCallback: z.boolean({
|
||||||
const env = () =>
|
const env = () =>
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
@@ -47,6 +66,13 @@ const env = () =>
|
|||||||
isEnabledDispatchOnMontonioCallback:
|
isEnabledDispatchOnMontonioCallback:
|
||||||
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
|
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.parse({
|
||||||
|
emailSender: process.env.EMAIL_SENDER,
|
||||||
|
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||||
|
isEnabledDispatchOnMontonioCallback:
|
||||||
|
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
|
||||||
|
});
|
||||||
|
|
||||||
const sendEmail = async ({
|
const sendEmail = async ({
|
||||||
account,
|
account,
|
||||||
@@ -60,9 +86,17 @@ const sendEmail = async ({
|
|||||||
analysisPackageName: string;
|
analysisPackageName: string;
|
||||||
partnerLocationName: string;
|
partnerLocationName: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
account: Pick<AccountWithParams, 'name' | 'id'>;
|
||||||
|
email: string;
|
||||||
|
analysisPackageName: string;
|
||||||
|
partnerLocationName: string;
|
||||||
|
language: string;
|
||||||
}) => {
|
}) => {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerAdminClient();
|
||||||
try {
|
try {
|
||||||
|
const { renderSynlabAnalysisPackageEmail } = await import(
|
||||||
|
'@kit/email-templates'
|
||||||
|
);
|
||||||
const { renderSynlabAnalysisPackageEmail } = await import(
|
const { renderSynlabAnalysisPackageEmail } = await import(
|
||||||
'@kit/email-templates'
|
'@kit/email-templates'
|
||||||
);
|
);
|
||||||
@@ -91,10 +125,15 @@ const sendEmail = async ({
|
|||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
body: html,
|
body: html,
|
||||||
});
|
});
|
||||||
|
await createNotificationsApi(client).createNotification({
|
||||||
|
account_id: account.id,
|
||||||
|
body: html,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to send email, message=${error}`);
|
throw new Error(`Failed to send email, message=${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
async function decodeOrderToken(orderToken: string) {
|
async function decodeOrderToken(orderToken: string) {
|
||||||
const secretKey = process.env.MONTONIO_SECRET_KEY as string;
|
const secretKey = process.env.MONTONIO_SECRET_KEY as string;
|
||||||
@@ -105,6 +144,7 @@ async function decodeOrderToken(orderToken: string) {
|
|||||||
|
|
||||||
if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) {
|
if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) {
|
||||||
throw new Error('Payment not successful');
|
throw new Error('Payment not successful');
|
||||||
|
throw new Error('Payment not successful');
|
||||||
}
|
}
|
||||||
|
|
||||||
return decoded;
|
return decoded;
|
||||||
@@ -114,35 +154,48 @@ async function getCartByOrderToken(decoded: MontonioOrderToken) {
|
|||||||
const [, , cartId] = decoded.merchantReferenceDisplay.split(':');
|
const [, , cartId] = decoded.merchantReferenceDisplay.split(':');
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
throw new Error('Cart ID not found');
|
throw new Error('Cart ID not found');
|
||||||
|
throw new Error('Cart ID not found');
|
||||||
}
|
}
|
||||||
const cart = await retrieveCart(cartId);
|
const cart = await retrieveCart(cartId);
|
||||||
if (!cart) {
|
if (!cart) {
|
||||||
throw new Error('Cart not found');
|
throw new Error('Cart not found');
|
||||||
|
throw new Error('Cart not found');
|
||||||
}
|
}
|
||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrderResultParameters(medusaOrder: StoreOrder) {
|
async function getOrderResultParameters(medusaOrder: StoreOrder) {
|
||||||
const { productTypes } = await listProductTypes();
|
const { productTypes } = await listProductTypes();
|
||||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
|
const analysisPackagesType = productTypes.find(
|
||||||
const analysisType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_TYPE_HANDLE);
|
({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE,
|
||||||
|
);
|
||||||
|
const analysisType = productTypes.find(
|
||||||
|
({ metadata }) => metadata?.handle === ANALYSIS_TYPE_HANDLE,
|
||||||
|
);
|
||||||
|
|
||||||
const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id);
|
const analysisPackageOrderItem = medusaOrder.items?.find(
|
||||||
const analysisItems = medusaOrder.items?.filter(({ product_type_id }) => product_type_id === analysisType?.id);
|
({ product_type_id }) => product_type_id === analysisPackagesType?.id,
|
||||||
|
);
|
||||||
|
const analysisItems = medusaOrder.items?.filter(
|
||||||
|
({ product_type_id }) => product_type_id === analysisType?.id,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
medusaOrderId: medusaOrder.id,
|
medusaOrderId: medusaOrder.id,
|
||||||
email: medusaOrder.email,
|
email: medusaOrder.email,
|
||||||
analysisPackageOrder: analysisPackageOrderItem
|
analysisPackageOrder: analysisPackageOrderItem
|
||||||
? {
|
? {
|
||||||
partnerLocationName: analysisPackageOrderItem?.metadata?.partner_location_name as string ?? '',
|
partnerLocationName:
|
||||||
|
(analysisPackageOrderItem?.metadata
|
||||||
|
?.partner_location_name as string) ?? '',
|
||||||
analysisPackageName: analysisPackageOrderItem?.title ?? '',
|
analysisPackageName: analysisPackageOrderItem?.title ?? '',
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
analysisItemsOrder: Array.isArray(analysisItems) && analysisItems.length > 0
|
analysisItemsOrder:
|
||||||
|
Array.isArray(analysisItems) && analysisItems.length > 0
|
||||||
? analysisItems.map(({ product }) => ({
|
? analysisItems.map(({ product }) => ({
|
||||||
analysisName: product?.title ?? '',
|
analysisName: product?.title ?? '',
|
||||||
analysisId: product?.metadata?.analysisIdOriginal as string ?? '',
|
analysisId: (product?.metadata?.analysisIdOriginal as string) ?? '',
|
||||||
}))
|
}))
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
@@ -153,12 +206,17 @@ async function sendAnalysisPackageOrderEmail({
|
|||||||
email,
|
email,
|
||||||
analysisPackageOrder,
|
analysisPackageOrder,
|
||||||
}: {
|
}: {
|
||||||
|
account: AccountWithParams;
|
||||||
|
email: string;
|
||||||
account: AccountWithParams;
|
account: AccountWithParams;
|
||||||
email: string;
|
email: string;
|
||||||
analysisPackageOrder: {
|
analysisPackageOrder: {
|
||||||
partnerLocationName: string;
|
partnerLocationName: string;
|
||||||
analysisPackageName: string;
|
analysisPackageName: string;
|
||||||
};
|
};
|
||||||
|
partnerLocationName: string;
|
||||||
|
analysisPackageName: string;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
const { language } = await createI18nServerInstance();
|
const { language } = await createI18nServerInstance();
|
||||||
const { analysisPackageName, partnerLocationName } = analysisPackageOrder;
|
const { analysisPackageName, partnerLocationName } = analysisPackageOrder;
|
||||||
@@ -170,6 +228,7 @@ async function sendAnalysisPackageOrderEmail({
|
|||||||
partnerLocationName,
|
partnerLocationName,
|
||||||
language,
|
language,
|
||||||
});
|
});
|
||||||
|
console.info(`Successfully sent analysis package order email to ${email}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send email', error);
|
console.error('Failed to send email', error);
|
||||||
}
|
}
|
||||||
@@ -179,6 +238,7 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
const { account } = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found in context');
|
throw new Error('Account not found in context');
|
||||||
|
throw new Error('Account not found in context');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -191,8 +251,12 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
const orderContainsSynlabItems = !!orderedAnalysisElements?.length;
|
const orderContainsSynlabItems = !!orderedAnalysisElements?.length;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingAnalysisOrder = await getAnalysisOrder({ medusaOrderId: medusaOrder.id });
|
const existingAnalysisOrder = await getAnalysisOrder({
|
||||||
console.info(`Analysis order already exists for medusaOrderId=${medusaOrder.id}, orderId=${existingAnalysisOrder.id}`);
|
medusaOrderId: medusaOrder.id,
|
||||||
|
});
|
||||||
|
console.info(
|
||||||
|
`Analysis order already exists for medusaOrderId=${medusaOrder.id}, orderId=${existingAnalysisOrder.id}`,
|
||||||
|
);
|
||||||
return { success: true, orderId: existingAnalysisOrder.id };
|
return { success: true, orderId: existingAnalysisOrder.id };
|
||||||
} catch {
|
} catch {
|
||||||
// ignored
|
// ignored
|
||||||
@@ -231,19 +295,26 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
if (analysisPackageOrder) {
|
if (analysisPackageOrder) {
|
||||||
await sendAnalysisPackageOrderEmail({ account, email, analysisPackageOrder });
|
await sendAnalysisPackageOrderEmail({
|
||||||
|
account,
|
||||||
|
email,
|
||||||
|
analysisPackageOrder,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.info(`Order has no analysis package, skipping email.`);
|
console.info(`Order has no analysis package, skipping email.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysisItemsOrder) {
|
if (analysisItemsOrder) {
|
||||||
// @TODO send email for separate analyses
|
// @TODO send email for separate analyses
|
||||||
console.warn(`Order has analysis items, but no email template exists yet`);
|
console.warn(
|
||||||
|
`Order has analysis items, but no email template exists yet`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.info(`Order has no analysis items, skipping email.`);
|
console.info(`Order has no analysis items, skipping email.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Missing email to send order result email', orderResult);
|
console.error('Missing email to send order result email', orderResult);
|
||||||
|
console.error('Missing email to send order result email', orderResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env().isEnabledDispatchOnMontonioCallback && orderContainsSynlabItems) {
|
if (env().isEnabledDispatchOnMontonioCallback && orderContainsSynlabItems) {
|
||||||
@@ -263,6 +334,7 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
|
|
||||||
return { success: true, orderId };
|
return { success: true, orderId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Failed to place order', error);
|
||||||
console.error('Failed to place order', error);
|
console.error('Failed to place order', error);
|
||||||
throw new Error(`Failed to place order, message=${error}`);
|
throw new Error(`Failed to place order, message=${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { processMontonioCallback } from './actions';
|
import { processMontonioCallback } from './actions';
|
||||||
|
|
||||||
export default function MontonioCallbackClient({ orderToken, error }: {
|
export default function MontonioCallbackClient({
|
||||||
|
orderToken,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
orderToken?: string;
|
orderToken?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}) {
|
}) {
|
||||||
@@ -34,10 +39,12 @@ export default function MontonioCallbackClient({ orderToken, error }: {
|
|||||||
return router.push(`/home/order/${result.orderId}/confirmed`);
|
return router.push(`/home/order/${result.orderId}/confirmed`);
|
||||||
}
|
}
|
||||||
if (result.failedServiceBookings?.length) {
|
if (result.failedServiceBookings?.length) {
|
||||||
router.push(`/home/cart/montonio-callback/error?reasonFailed=${result.failedServiceBookings.map(({reason}) => reason).join(',')}`);
|
router.push(
|
||||||
|
`/home/cart/montonio-callback/error?reasonFailed=${result.failedServiceBookings.map(({ reason }) => reason).join(',')}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to place order", error);
|
console.error('Failed to place order', error);
|
||||||
router.push('/home/cart/montonio-callback/error');
|
router.push('/home/cart/montonio-callback/error');
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
@@ -48,9 +55,9 @@ export default function MontonioCallbackClient({ orderToken, error }: {
|
|||||||
}, [orderToken, error, router, hasProcessed, isProcessing]);
|
}, [orderToken, error, router, hasProcessed, isProcessing]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex mt-10 justify-center min-h-screen">
|
<div className="mt-10 flex min-h-screen justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
<div className="border-primary mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-b-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { use } from 'react';
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
|
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
|
||||||
|
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Alert, AlertDescription } from '@kit/ui/shadcn/alert';
|
import { Alert, AlertDescription } from '@kit/ui/shadcn/alert';
|
||||||
import { AlertTitle } from '@kit/ui/shadcn/alert';
|
import { AlertTitle } from '@kit/ui/shadcn/alert';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import MontonioCallbackClient from './client-component';
|
import MontonioCallbackClient from './client-component';
|
||||||
|
|
||||||
export default async function MontonioCallbackPage({ searchParams }: {
|
export default async function MontonioCallbackPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
'order-token'?: string;
|
'order-token'?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { listProductTypes } from '@lib/data/products';
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
import { getCartReservations } from '~/lib/services/reservation.service';
|
||||||
import { findProductTypeIdByHandle } from '~/lib/utils';
|
import { findProductTypeIdByHandle } from '~/lib/utils';
|
||||||
|
|
||||||
import { getCartReservations } from '~/lib/services/reservation.service';
|
|
||||||
import Cart from '../../_components/cart';
|
import Cart from '../../_components/cart';
|
||||||
import CartTimer from '../../_components/cart/cart-timer';
|
import CartTimer from '../../_components/cart/cart-timer';
|
||||||
import { EnrichedCartItem } from '../../_components/cart/types';
|
import { EnrichedCartItem } from '../../_components/cart/types';
|
||||||
@@ -25,6 +25,7 @@ export async function generateMetadata() {
|
|||||||
|
|
||||||
async function CartPage() {
|
async function CartPage() {
|
||||||
const cart = await retrieveCart().catch((error) => {
|
const cart = await retrieveCart().catch((error) => {
|
||||||
|
console.error('Failed to retrieve cart', error);
|
||||||
console.error('Failed to retrieve cart', error);
|
console.error('Failed to retrieve cart', error);
|
||||||
return notFound();
|
return notFound();
|
||||||
});
|
});
|
||||||
@@ -74,6 +75,11 @@ async function CartPage() {
|
|||||||
synlabAnalyses={synlabAnalyses}
|
synlabAnalyses={synlabAnalyses}
|
||||||
ttoServiceItems={ttoServiceItems}
|
ttoServiceItems={ttoServiceItems}
|
||||||
/>
|
/>
|
||||||
|
<Cart
|
||||||
|
cart={cart}
|
||||||
|
synlabAnalyses={synlabAnalyses}
|
||||||
|
ttoServiceItems={ttoServiceItems}
|
||||||
|
/>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Scale } from 'lucide-react';
|
import { Scale } from 'lucide-react';
|
||||||
|
|
||||||
|
import SelectAnalysisPackages from '@kit/shared/components/select-analysis-packages';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { PageBody } from '@kit/ui/page';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import SelectAnalysisPackages from '@kit/shared/components/select-analysis-packages';
|
|
||||||
import { PageBody } from '@kit/ui/page';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
import ComparePackagesModal from '../../_components/compare-packages-modal';
|
import ComparePackagesModal from '../../_components/compare-packages-modal';
|
||||||
import { loadAnalysisPackages } from '../../_lib/server/load-analysis-packages';
|
import { loadAnalysisPackages } from '../../_lib/server/load-analysis-packages';
|
||||||
|
|
||||||
@@ -21,7 +21,8 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function OrderAnalysisPackagePage() {
|
async function OrderAnalysisPackagePage() {
|
||||||
const { analysisPackageElements, analysisPackages, countryCode } = await loadAnalysisPackages();
|
const { analysisPackageElements, analysisPackages, countryCode } =
|
||||||
|
await loadAnalysisPackages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
@@ -40,7 +41,10 @@ async function OrderAnalysisPackagePage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectAnalysisPackages analysisPackages={analysisPackages} countryCode={countryCode} />
|
<SelectAnalysisPackages
|
||||||
|
analysisPackages={analysisPackages}
|
||||||
|
countryCode={countryCode}
|
||||||
|
/>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
import { PageBody } from '@kit/ui/page';
|
import { PageBody } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
import {
|
||||||
|
PageViewAction,
|
||||||
|
createPageViewLog,
|
||||||
|
} from '~/lib/services/audit/pageView.service';
|
||||||
|
|
||||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
|
||||||
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
||||||
import { createPageViewLog, PageViewAction } from '~/lib/services/audit/pageView.service';
|
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||||
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { PageBody } from '@kit/ui/page';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { PageBody } from '@kit/ui/page';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
|
||||||
|
|
||||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
import { getAnalysisOrder } from '~/lib/services/order.service';
|
|
||||||
import { retrieveOrder } from '@lib/data/orders';
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
import Divider from "@modules/common/components/divider"
|
|
||||||
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
|
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
|
||||||
import OrderItems from '@/app/home/(user)/_components/order/order-items';
|
import OrderItems from '@/app/home/(user)/_components/order/order-items';
|
||||||
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
|
import { retrieveOrder } from '@lib/data/orders';
|
||||||
|
import Divider from '@modules/common/components/divider';
|
||||||
|
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
import { getAnalysisOrder } from '~/lib/services/order.service';
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
@@ -27,12 +27,16 @@ async function OrderConfirmedPage(props: {
|
|||||||
}) {
|
}) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
const order = await getAnalysisOrder({ analysisOrderId: Number(params.orderId) }).catch(() => null);
|
const order = await getAnalysisOrder({
|
||||||
|
analysisOrderId: Number(params.orderId),
|
||||||
|
}).catch(() => null);
|
||||||
if (!order) {
|
if (!order) {
|
||||||
redirect(pathsConfig.app.myOrders);
|
redirect(pathsConfig.app.myOrders);
|
||||||
}
|
}
|
||||||
|
|
||||||
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
|
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
if (!medusaOrder) {
|
if (!medusaOrder) {
|
||||||
redirect(pathsConfig.app.myOrders);
|
redirect(pathsConfig.app.myOrders);
|
||||||
}
|
}
|
||||||
@@ -41,7 +45,7 @@ async function OrderConfirmedPage(props: {
|
|||||||
<PageBody>
|
<PageBody>
|
||||||
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
|
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
|
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 gap-y-6 lg:px-4">
|
||||||
<OrderDetails order={order} />
|
<OrderDetails order={order} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<OrderItems medusaOrder={medusaOrder} />
|
<OrderItems medusaOrder={medusaOrder} />
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
|
||||||
|
|
||||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
import { getAnalysisOrder } from '~/lib/services/order.service';
|
|
||||||
import { retrieveOrder } from '@lib/data/orders';
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
import Divider from "@modules/common/components/divider"
|
|
||||||
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
|
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
|
||||||
import OrderItems from '@/app/home/(user)/_components/order/order-items';
|
import OrderItems from '@/app/home/(user)/_components/order/order-items';
|
||||||
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
|
import { retrieveOrder } from '@lib/data/orders';
|
||||||
|
import Divider from '@modules/common/components/divider';
|
||||||
|
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
import { getAnalysisOrder } from '~/lib/services/order.service';
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
@@ -27,12 +27,16 @@ async function OrderConfirmedPage(props: {
|
|||||||
}) {
|
}) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
const order = await getAnalysisOrder({ analysisOrderId: Number(params.orderId) }).catch(() => null);
|
const order = await getAnalysisOrder({
|
||||||
|
analysisOrderId: Number(params.orderId),
|
||||||
|
}).catch(() => null);
|
||||||
if (!order) {
|
if (!order) {
|
||||||
redirect(pathsConfig.app.myOrders);
|
redirect(pathsConfig.app.myOrders);
|
||||||
}
|
}
|
||||||
|
|
||||||
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
|
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
if (!medusaOrder) {
|
if (!medusaOrder) {
|
||||||
redirect(pathsConfig.app.myOrders);
|
redirect(pathsConfig.app.myOrders);
|
||||||
}
|
}
|
||||||
@@ -41,7 +45,7 @@ async function OrderConfirmedPage(props: {
|
|||||||
<PageBody>
|
<PageBody>
|
||||||
<PageHeader title={<Trans i18nKey="cart:order.title" />} />
|
<PageHeader title={<Trans i18nKey="cart:order.title" />} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
|
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 gap-y-6 lg:px-4">
|
||||||
<OrderDetails order={order} />
|
<OrderDetails order={order} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<OrderItems medusaOrder={medusaOrder} />
|
<OrderItems medusaOrder={medusaOrder} />
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
import { listProductTypes } from '@lib/data/products';
|
import { listProductTypes } from '@lib/data/products';
|
||||||
import { Divider } from '@medusajs/ui';
|
import { Divider } from '@medusajs/ui';
|
||||||
|
|
||||||
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
import { Divider } from '@medusajs/ui';
|
||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { PageBody } from '@kit/ui/makerkit/page';
|
import { PageBody } from '@kit/ui/makerkit/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
@@ -87,6 +92,7 @@ async function OrdersPage() {
|
|||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
{analysisOrders.length === 0 && (
|
{analysisOrders.length === 0 && (
|
||||||
<h5 className="mt-6">
|
<h5 className="mt-6">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { toTitleCase } from '@/lib/utils';
|
import { toTitleCase } from '@/lib/utils';
|
||||||
@@ -5,12 +7,15 @@ import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-
|
|||||||
|
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import { createUserAnalysesApi } from '@kit/user-analyses/api';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import Dashboard from '../_components/dashboard';
|
import Dashboard from '../_components/dashboard';
|
||||||
import DashboardCards from '../_components/dashboard-cards';
|
import DashboardCards from '../_components/dashboard-cards';
|
||||||
|
import Recommendations from '../_components/recommendations';
|
||||||
|
import RecommendationsSkeleton from '../_components/recommendations-skeleton';
|
||||||
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
|
||||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
|
|
||||||
@@ -48,6 +53,17 @@ async function UserHomePage() {
|
|||||||
/>
|
/>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Dashboard account={account} bmiThresholds={bmiThresholds} />
|
<Dashboard account={account} bmiThresholds={bmiThresholds} />
|
||||||
|
{process.env.OPENAI_API_KEY &&
|
||||||
|
process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS && (
|
||||||
|
<>
|
||||||
|
<h4>
|
||||||
|
<Trans i18nKey="dashboard:recommendations.title" />
|
||||||
|
</h4>
|
||||||
|
<Suspense fallback={<RecommendationsSkeleton />}>
|
||||||
|
<Recommendations account={account} />
|
||||||
|
</Suspense>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const BookingProvider: React.FC<{
|
|||||||
const [locations, setLocations] = useState<Location[] | null>(null);
|
const [locations, setLocations] = useState<Location[] | null>(null);
|
||||||
const [isLoadingTimeSlots, setIsLoadingTimeSlots] = useState(false);
|
const [isLoadingTimeSlots, setIsLoadingTimeSlots] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let metadataServiceIds = [];
|
let metadataServiceIds = [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useForm } from "react-hook-form";
|
import { z } from 'zod';
|
||||||
import { z } from "zod";
|
|
||||||
import { StoreCart, StoreCartLineItem } from "@medusajs/types"
|
import { Form } from '@kit/ui/form';
|
||||||
import { Form } from "@kit/ui/form";
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -17,29 +17,39 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@kit/ui/select';
|
} from '@kit/ui/select';
|
||||||
import { updateCartPartnerLocation } from '../../_lib/server/update-cart-partner-location';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { updateCartPartnerLocation } from '../../_lib/server/update-cart-partner-location';
|
||||||
import partnerLocations from './partner-locations.json';
|
import partnerLocations from './partner-locations.json';
|
||||||
|
|
||||||
const AnalysisLocationSchema = z.object({
|
const AnalysisLocationSchema = z.object({
|
||||||
locationId: z.string().min(1),
|
locationId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: StoreCart, synlabAnalyses: StoreCartLineItem[] }) {
|
export default function AnalysisLocation({
|
||||||
|
cart,
|
||||||
|
synlabAnalyses,
|
||||||
|
}: {
|
||||||
|
cart: StoreCart;
|
||||||
|
synlabAnalyses: StoreCartLineItem[];
|
||||||
|
}) {
|
||||||
const { t } = useTranslation('cart');
|
const { t } = useTranslation('cart');
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof AnalysisLocationSchema>>({
|
const form = useForm<z.infer<typeof AnalysisLocationSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
locationId: cart.metadata?.partner_location_id as string ?? '',
|
locationId: (cart.metadata?.partner_location_id as string) ?? '',
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AnalysisLocationSchema),
|
resolver: zodResolver(AnalysisLocationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getLocation = (locationId: string) => partnerLocations.find(({ name }) => name === locationId);
|
const getLocation = (locationId: string) =>
|
||||||
|
partnerLocations.find(({ name }) => name === locationId);
|
||||||
|
|
||||||
const selectedLocation = getLocation(form.watch('locationId'));
|
const selectedLocation = getLocation(form.watch('locationId'));
|
||||||
|
|
||||||
const onSubmit = async ({ locationId }: z.infer<typeof AnalysisLocationSchema>) => {
|
const onSubmit = async ({
|
||||||
|
locationId,
|
||||||
|
}: z.infer<typeof AnalysisLocationSchema>) => {
|
||||||
const promise = updateCartPartnerLocation({
|
const promise = updateCartPartnerLocation({
|
||||||
cartId: cart.id,
|
cartId: cart.id,
|
||||||
lineIds: synlabAnalyses.map(({ id }) => id),
|
lineIds: synlabAnalyses.map(({ id }) => id),
|
||||||
@@ -52,18 +62,18 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
|
|||||||
loading: t(`cart:items.analysisLocation.loading`),
|
loading: t(`cart:items.analysisLocation.loading`),
|
||||||
error: t(`cart:items.analysisLocation.error`),
|
error: t(`cart:items.analysisLocation.error`),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
|
<div className="txt-medium flex h-full w-full flex-col gap-y-4 bg-white">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-muted-foreground text-sm">
|
||||||
<Trans i18nKey={'cart:locations.description'} />
|
<Trans i18nKey={'cart:locations.description'} />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit((data) => onSubmit(data))}
|
onSubmit={form.handleSubmit((data) => onSubmit(data))}
|
||||||
className="w-full mb-2 flex gap-x-2 flex-1"
|
className="mb-2 flex w-full flex-1 gap-x-2"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={form.watch('locationId')}
|
value={form.watch('locationId')}
|
||||||
@@ -82,16 +92,24 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{Object.entries(partnerLocations
|
{Object.entries(
|
||||||
.reduce((acc, curr) => ({
|
partnerLocations.reduce(
|
||||||
|
(acc, curr) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[curr.city]: [...((acc[curr.city] as typeof partnerLocations) ?? []), curr],
|
[curr.city]: [
|
||||||
}), {} as Record<string, typeof partnerLocations>))
|
...((acc[curr.city] as typeof partnerLocations) ?? []),
|
||||||
.map(([city, locations]) => (
|
curr,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{} as Record<string, typeof partnerLocations>,
|
||||||
|
),
|
||||||
|
).map(([city, locations]) => (
|
||||||
<SelectGroup key={city}>
|
<SelectGroup key={city}>
|
||||||
<SelectLabel>{city}</SelectLabel>
|
<SelectLabel>{city}</SelectLabel>
|
||||||
{locations.map((location) => (
|
{locations.map((location) => (
|
||||||
<SelectItem key={location.name} value={location.name}>{location.name}</SelectItem>
|
<SelectItem key={location.name} value={location.name}>
|
||||||
|
{location.name}
|
||||||
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
))}
|
))}
|
||||||
@@ -101,15 +119,11 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{selectedLocation && (
|
{selectedLocation && (
|
||||||
<div className="flex flex-col gap-y-2 mb-4">
|
<div className="mb-4 flex flex-col gap-y-2">
|
||||||
<p className="text-sm">
|
<p className="text-sm">{selectedLocation.address}</p>
|
||||||
{selectedLocation.address}
|
<p className="text-sm">{selectedLocation.hours}</p>
|
||||||
</p>
|
|
||||||
<p className="text-sm">
|
|
||||||
{selectedLocation.hours}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { Trash } from "lucide-react";
|
import { useState } from 'react';
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { Spinner } from '@medusajs/icons';
|
||||||
|
import { Trash } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { Spinner } from "@medusajs/icons";
|
import { handleDeleteCartItem } from '~/lib/services/medusaCart.service';
|
||||||
import { handleDeleteCartItem } from "~/lib/services/medusaCart.service";
|
|
||||||
|
|
||||||
const CartItemDelete = ({
|
const CartItemDelete = ({
|
||||||
id,
|
id,
|
||||||
@@ -33,9 +34,9 @@ const CartItemDelete = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between text-small-regular">
|
<div className="text-small-regular flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
|
className="text-ui-fg-subtle hover:text-ui-fg-base flex cursor-pointer gap-x-1"
|
||||||
onClick={() => handleDelete()}
|
onClick={() => handleDelete()}
|
||||||
>
|
>
|
||||||
{isDeleting ? <Spinner className="animate-spin" /> : <Trash />}
|
{isDeleting ? <Spinner className="animate-spin" /> : <Trash />}
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||||
import { useTranslation } from "react-i18next"
|
import { HttpTypes } from '@medusajs/types';
|
||||||
import {
|
import { useTranslation } from 'react-i18next';
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
} from '@kit/ui/table';
|
|
||||||
import { formatCurrency } from "@/packages/shared/src/utils"
|
|
||||||
import CartItemDelete from "./cart-item-delete";
|
|
||||||
|
|
||||||
export default function CartItem({ item, currencyCode }: {
|
import { TableCell, TableRow } from '@kit/ui/table';
|
||||||
item: HttpTypes.StoreCartLineItem
|
|
||||||
currencyCode: string
|
import CartItemDelete from './cart-item-delete';
|
||||||
|
|
||||||
|
export default function CartItem({
|
||||||
|
item,
|
||||||
|
currencyCode,
|
||||||
|
}: {
|
||||||
|
item: HttpTypes.StoreCartLineItem;
|
||||||
|
currencyCode: string;
|
||||||
}) {
|
}) {
|
||||||
const { i18n: { language } } = useTranslation();
|
const {
|
||||||
|
i18n: { language },
|
||||||
|
} = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow className="w-full" data-testid="product-row">
|
<TableRow className="w-full" data-testid="product-row">
|
||||||
<TableCell className="text-left w-[100%] px-4 sm:px-6">
|
<TableCell className="w-[100%] px-4 text-left sm:px-6">
|
||||||
<p
|
<p
|
||||||
className="txt-medium-plus text-ui-fg-base"
|
className="txt-medium-plus text-ui-fg-base"
|
||||||
data-testid="product-title"
|
data-testid="product-title"
|
||||||
@@ -26,9 +30,7 @@ export default function CartItem({ item, currencyCode }: {
|
|||||||
</p>
|
</p>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-4 sm:px-6">
|
<TableCell className="px-4 sm:px-6">{item.quantity}</TableCell>
|
||||||
{item.quantity}
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-4 sm:px-6">
|
<TableCell className="min-w-[80px] px-4 sm:px-6">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
@@ -38,7 +40,7 @@ export default function CartItem({ item, currencyCode }: {
|
|||||||
})}
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-4 sm:px-6 text-right">
|
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: item.total,
|
value: item.total,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
@@ -46,11 +48,11 @@ export default function CartItem({ item, currencyCode }: {
|
|||||||
})}
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right px-4 sm:px-6">
|
<TableCell className="px-4 text-right sm:px-6">
|
||||||
<span className="flex gap-x-1 justify-end w-[60px]">
|
<span className="flex w-[60px] justify-end gap-x-1">
|
||||||
<CartItemDelete id={item.id} />
|
<CartItemDelete id={item.id} />
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { StoreCart, StoreCartLineItem } from "@medusajs/types"
|
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
import CartItem from "./cart-item";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
|
||||||
TableHeader,
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
} from '@kit/ui/table';
|
} from '@kit/ui/table';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export default function CartItems({ cart, items, productColumnLabelKey }: {
|
import CartItem from './cart-item';
|
||||||
|
|
||||||
|
export default function CartItems({
|
||||||
|
cart,
|
||||||
|
items,
|
||||||
|
productColumnLabelKey,
|
||||||
|
}: {
|
||||||
cart: StoreCart;
|
cart: StoreCart;
|
||||||
items: StoreCartLineItem[];
|
items: StoreCartLineItem[];
|
||||||
productColumnLabelKey: string;
|
productColumnLabelKey: string;
|
||||||
@@ -19,7 +25,7 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="rounded-lg border border-separate">
|
<Table className="border-separate rounded-lg border">
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-4 sm:px-6">
|
<TableHead className="px-4 sm:px-6">
|
||||||
@@ -28,19 +34,20 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
|
|||||||
<TableHead className="px-4 sm:px-6">
|
<TableHead className="px-4 sm:px-6">
|
||||||
<Trans i18nKey="cart:table.quantity" />
|
<Trans i18nKey="cart:table.quantity" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4 sm:px-6 min-w-[100px]">
|
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
||||||
<Trans i18nKey="cart:table.price" />
|
<Trans i18nKey="cart:table.price" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4 sm:px-6 min-w-[100px] text-right">
|
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
||||||
<Trans i18nKey="cart:table.total" />
|
<Trans i18nKey="cart:table.total" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4 sm:px-6">
|
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items
|
{items
|
||||||
.sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1)
|
.sort((a, b) =>
|
||||||
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
|
)
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<CartItem
|
<CartItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -50,5 +57,5 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,11 +127,16 @@ export default function CartServiceItem({
|
|||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{isUnavailable && <TableRow>
|
{isUnavailable && (
|
||||||
<TableCell colSpan={8} className="text-destructive px-4 text-left sm:px-6">
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={8}
|
||||||
|
className="text-destructive px-4 text-left sm:px-6"
|
||||||
|
>
|
||||||
<Trans i18nKey="booking:timeSlotUnavailable" />
|
<Trans i18nKey="booking:timeSlotUnavailable" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>}
|
</TableRow>
|
||||||
|
)}
|
||||||
<EditCartServiceItemModal
|
<EditCartServiceItemModal
|
||||||
item={editingItem}
|
item={editingItem}
|
||||||
onComplete={() => setEditingItem(null)}
|
onComplete={() => setEditingItem(null)}
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ export default function CartServiceItems({
|
|||||||
cart,
|
cart,
|
||||||
items,
|
items,
|
||||||
productColumnLabelKey,
|
productColumnLabelKey,
|
||||||
unavailableLineItemIds
|
unavailableLineItemIds,
|
||||||
}: {
|
}: {
|
||||||
cart: StoreCart;
|
cart: StoreCart;
|
||||||
items: EnrichedCartItem[];
|
items: EnrichedCartItem[];
|
||||||
productColumnLabelKey: string;
|
productColumnLabelKey: string;
|
||||||
unavailableLineItemIds?: string[]
|
unavailableLineItemIds?: string[];
|
||||||
}) {
|
}) {
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { handleLineItemTimeout } from '@/lib/services/medusaCart.service';
|
||||||
|
import { StoreCartLineItem } from '@medusajs/types';
|
||||||
|
import { Timer } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -8,18 +14,17 @@ import {
|
|||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle,
|
||||||
} from "@kit/ui/alert-dialog";
|
} from '@kit/ui/alert-dialog';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
import { Timer } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { StoreCartLineItem } from '@medusajs/types';
|
|
||||||
import { handleLineItemTimeout } from '@/lib/services/medusaCart.service';
|
|
||||||
|
|
||||||
const TIMEOUT_MINUTES = 15;
|
const TIMEOUT_MINUTES = 15;
|
||||||
|
|
||||||
export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem }) {
|
export default function CartTimer({
|
||||||
|
cartItem,
|
||||||
|
}: {
|
||||||
|
cartItem: StoreCartLineItem;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [timeLeft, setTimeLeft] = useState<number | null>(null);
|
const [timeLeft, setTimeLeft] = useState<number | null>(null);
|
||||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -39,7 +44,9 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [updatedAt]);
|
}, [updatedAt]);
|
||||||
|
|
||||||
const minutes = timeLeft ? Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)) : 0;
|
const minutes = timeLeft
|
||||||
|
? Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60))
|
||||||
|
: 0;
|
||||||
const seconds = timeLeft ? Math.floor((timeLeft % (1000 * 60)) / 1000) : 0;
|
const seconds = timeLeft ? Math.floor((timeLeft % (1000 * 60)) / 1000) : 0;
|
||||||
|
|
||||||
const isTimeLeftPositive = timeLeft === null || timeLeft > 0;
|
const isTimeLeftPositive = timeLeft === null || timeLeft > 0;
|
||||||
@@ -53,13 +60,16 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
|||||||
}, [isTimeLeftPositive, cartItem.id]);
|
}, [isTimeLeftPositive, cartItem.id]);
|
||||||
|
|
||||||
if (timeLeft === null) {
|
if (timeLeft === null) {
|
||||||
return <div className='min-h-[40px]' />;
|
return <div className="min-h-[40px]" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<Button variant="outline" className="flex items-center gap-x-2 bg-accent hover:bg-accent px-4 cursor-default">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-accent hover:bg-accent flex cursor-default items-center gap-x-2 px-4"
|
||||||
|
>
|
||||||
<Timer />
|
<Timer />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{t('cart:checkout.timeLeft', {
|
{t('cart:checkout.timeLeft', {
|
||||||
@@ -76,7 +86,9 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
|||||||
{t('cart:checkout.timeoutTitle')}
|
{t('cart:checkout.timeoutTitle')}
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{t('cart:checkout.timeoutDescription', { productTitle: cartItem.product?.title })}
|
{t('cart:checkout.timeoutDescription', {
|
||||||
|
productTitle: cartItem.product?.title,
|
||||||
|
})}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@@ -87,5 +99,5 @@ export default function CartTimer({ cartItem }: { cartItem: StoreCartLineItem })
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server"
|
'use server';
|
||||||
|
|
||||||
import { applyPromotions } from "@lib/data/cart"
|
import { applyPromotions } from '@lib/data/cart';
|
||||||
|
|
||||||
export async function addPromotionCodeAction(code: string) {
|
export async function addPromotionCodeAction(code: string) {
|
||||||
try {
|
try {
|
||||||
@@ -12,9 +12,14 @@ export async function addPromotionCodeAction(code: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removePromotionCodeAction(codeToRemove: string, appliedCodes: string[]) {
|
export async function removePromotionCodeAction(
|
||||||
|
codeToRemove: string,
|
||||||
|
appliedCodes: string[],
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const updatedCodes = appliedCodes.filter((appliedCode) => appliedCode !== codeToRemove);
|
const updatedCodes = appliedCodes.filter(
|
||||||
|
(appliedCode) => appliedCode !== codeToRemove,
|
||||||
|
);
|
||||||
await applyPromotions(updatedCodes);
|
await applyPromotions(updatedCodes);
|
||||||
return { success: true, message: 'Discount code removed successfully' };
|
return { success: true, message: 'Discount code removed successfully' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,30 +1,37 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import { Badge, Text } from "@medusajs/ui"
|
import React from 'react';
|
||||||
import { toast } from '@kit/ui/sonner';
|
|
||||||
import React from "react";
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { convertToLocale } from '@lib/util/money';
|
||||||
|
import { StoreCart, StorePromotion } from '@medusajs/types';
|
||||||
|
import { Badge, Text } from '@medusajs/ui';
|
||||||
|
import Trash from '@modules/common/icons/trash';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { convertToLocale } from "@lib/util/money"
|
|
||||||
import { StoreCart, StorePromotion } from "@medusajs/types"
|
|
||||||
import Trash from "@modules/common/icons/trash"
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Form, FormControl, FormField, FormItem } from "@kit/ui/form";
|
import { Form, FormControl, FormField, FormItem } from '@kit/ui/form';
|
||||||
|
import { Input } from '@kit/ui/input';
|
||||||
|
import { toast } from '@kit/ui/sonner';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { Input } from "@kit/ui/input";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import {
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
addPromotionCodeAction,
|
||||||
import { useForm } from "react-hook-form";
|
removePromotionCodeAction,
|
||||||
import { z } from "zod";
|
} from './discount-code-actions';
|
||||||
import { addPromotionCodeAction, removePromotionCodeAction } from "./discount-code-actions";
|
|
||||||
|
|
||||||
const DiscountCodeSchema = z.object({
|
const DiscountCodeSchema = z.object({
|
||||||
code: z.string().min(1),
|
code: z.string().min(1),
|
||||||
})
|
});
|
||||||
|
|
||||||
export default function DiscountCode({ cart }: {
|
export default function DiscountCode({
|
||||||
|
cart,
|
||||||
|
}: {
|
||||||
cart: StoreCart & {
|
cart: StoreCart & {
|
||||||
promotions: StorePromotion[]
|
promotions: StorePromotion[];
|
||||||
}
|
};
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('cart');
|
const { t } = useTranslation('cart');
|
||||||
|
|
||||||
@@ -33,11 +40,11 @@ export default function DiscountCode({ cart }: {
|
|||||||
const removePromotionCode = async (code: string) => {
|
const removePromotionCode = async (code: string) => {
|
||||||
const appliedCodes = promotions
|
const appliedCodes = promotions
|
||||||
.filter((p) => p.code !== undefined)
|
.filter((p) => p.code !== undefined)
|
||||||
.map((p) => p.code!)
|
.map((p) => p.code!);
|
||||||
|
|
||||||
const loading = toast.loading(t('cart:discountCode.removeLoading'));
|
const loading = toast.loading(t('cart:discountCode.removeLoading'));
|
||||||
|
|
||||||
const result = await removePromotionCodeAction(code, appliedCodes)
|
const result = await removePromotionCodeAction(code, appliedCodes);
|
||||||
|
|
||||||
toast.dismiss(loading);
|
toast.dismiss(loading);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -45,21 +52,20 @@ export default function DiscountCode({ cart }: {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(t('cart:discountCode.removeError'));
|
toast.error(t('cart:discountCode.removeError'));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const addPromotionCode = async (code: string) => {
|
const addPromotionCode = async (code: string) => {
|
||||||
const loading = toast.loading(t('cart:discountCode.addLoading'));
|
const loading = toast.loading(t('cart:discountCode.addLoading'));
|
||||||
const result = await addPromotionCodeAction(code)
|
const result = await addPromotionCodeAction(code);
|
||||||
|
|
||||||
toast.dismiss(loading);
|
toast.dismiss(loading);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(t('cart:discountCode.addSuccess'));
|
toast.success(t('cart:discountCode.addSuccess'));
|
||||||
form.reset()
|
form.reset();
|
||||||
} else {
|
} else {
|
||||||
toast.error(t('cart:discountCode.addError'));
|
toast.error(t('cart:discountCode.addError'));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof DiscountCodeSchema>>({
|
const form = useForm<z.infer<typeof DiscountCodeSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -69,40 +75,41 @@ export default function DiscountCode({ cart }: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
|
<div className="txt-medium flex h-full w-full flex-col gap-y-4 bg-white">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-muted-foreground text-sm">
|
||||||
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
||||||
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2 flex-1"
|
className="mb-2 flex w-full flex-1 flex-col gap-x-2 gap-y-2 sm:flex-row"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name={'code'}
|
name={'code'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex-1">
|
<FormItem className="flex-1">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input required type="text" {...field} placeholder={t('cart:discountCode.placeholder')} />
|
<Input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
placeholder={t('cart:discountCode.placeholder')}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" variant="secondary" className="h-min">
|
||||||
type="submit"
|
|
||||||
variant="secondary"
|
|
||||||
className="h-min"
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'cart:discountCode.apply'} />
|
<Trans i18nKey={'cart:discountCode.apply'} />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{promotions.length > 0 && (
|
{promotions.length > 0 && (
|
||||||
<div className="w-full flex items-center mt-4">
|
<div className="mt-4 flex w-full items-center">
|
||||||
<div className="flex flex-col w-full gap-y-2">
|
<div className="flex w-full flex-col gap-y-2">
|
||||||
<p>
|
<p>
|
||||||
<Trans i18nKey={'cart:discountCode.appliedCodes'} />
|
<Trans i18nKey={'cart:discountCode.appliedCodes'} />
|
||||||
</p>
|
</p>
|
||||||
@@ -111,31 +118,31 @@ export default function DiscountCode({ cart }: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={promotion.id}
|
key={promotion.id}
|
||||||
className="flex items-center justify-between w-full max-w-full mb-2"
|
className="mb-2 flex w-full max-w-full items-center justify-between"
|
||||||
data-testid="discount-row"
|
data-testid="discount-row"
|
||||||
>
|
>
|
||||||
<Text className="flex gap-x-1 items-baseline text-sm w-4/5 pr-1">
|
<Text className="flex w-4/5 items-baseline gap-x-1 pr-1 text-sm">
|
||||||
<span className="truncate" data-testid="discount-code">
|
<span className="truncate" data-testid="discount-code">
|
||||||
<Badge
|
<Badge
|
||||||
color={promotion.is_automatic ? "green" : "grey"}
|
color={promotion.is_automatic ? 'green' : 'grey'}
|
||||||
size="small"
|
size="small"
|
||||||
className="px-4 text-sm"
|
className="px-4 text-sm"
|
||||||
>
|
>
|
||||||
{promotion.code}
|
{promotion.code}
|
||||||
</Badge>{" "}
|
</Badge>{' '}
|
||||||
(
|
(
|
||||||
{promotion.application_method?.value !== undefined &&
|
{promotion.application_method?.value !== undefined &&
|
||||||
promotion.application_method.currency_code !==
|
promotion.application_method.currency_code !==
|
||||||
undefined && (
|
undefined && (
|
||||||
<>
|
<>
|
||||||
{promotion.application_method.type ===
|
{promotion.application_method.type === 'percentage'
|
||||||
"percentage"
|
|
||||||
? `${promotion.application_method.value}%`
|
? `${promotion.application_method.value}%`
|
||||||
: convertToLocale({
|
: convertToLocale({
|
||||||
amount: Number(promotion.application_method.value),
|
amount: Number(
|
||||||
|
promotion.application_method.value,
|
||||||
|
),
|
||||||
currency_code:
|
currency_code:
|
||||||
promotion.application_method
|
promotion.application_method.currency_code,
|
||||||
.currency_code,
|
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -152,10 +159,10 @@ export default function DiscountCode({ cart }: {
|
|||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!promotion.code) {
|
if (!promotion.code) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
removePromotionCode(promotion.code)
|
removePromotionCode(promotion.code);
|
||||||
}}
|
}}
|
||||||
data-testid="remove-discount-button"
|
data-testid="remove-discount-button"
|
||||||
>
|
>
|
||||||
@@ -166,11 +173,11 @@ export default function DiscountCode({ cart }: {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { handleNavigateToPayment } from '@/lib/services/medusaCart.service';
|
||||||
|
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||||
|
import { initiatePaymentSession } from '@lib/data/cart';
|
||||||
|
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -26,6 +36,7 @@ export default function Cart({
|
|||||||
synlabAnalyses,
|
synlabAnalyses,
|
||||||
ttoServiceItems,
|
ttoServiceItems,
|
||||||
}: {
|
}: {
|
||||||
|
cart: StoreCart | null;
|
||||||
cart: StoreCart | null;
|
cart: StoreCart | null;
|
||||||
synlabAnalyses: StoreCartLineItem[];
|
synlabAnalyses: StoreCartLineItem[];
|
||||||
ttoServiceItems: EnrichedCartItem[];
|
ttoServiceItems: EnrichedCartItem[];
|
||||||
@@ -33,6 +44,9 @@ export default function Cart({
|
|||||||
const {
|
const {
|
||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useTranslation();
|
} = useTranslation();
|
||||||
|
const {
|
||||||
|
i18n: { language },
|
||||||
|
} = useTranslation();
|
||||||
|
|
||||||
const [isInitiatingSession, setIsInitiatingSession] = useState(false);
|
const [isInitiatingSession, setIsInitiatingSession] = useState(false);
|
||||||
const [unavailableLineItemIds, setUnavailableLineItemIds] = useState<string[]>()
|
const [unavailableLineItemIds, setUnavailableLineItemIds] = useState<string[]>()
|
||||||
@@ -43,6 +57,10 @@ export default function Cart({
|
|||||||
return (
|
return (
|
||||||
<div className="content-container py-5 lg:px-4">
|
<div className="content-container py-5 lg:px-4">
|
||||||
<div>
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center"
|
||||||
|
data-testid="empty-cart-message"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center justify-center"
|
className="flex flex-col items-center justify-center"
|
||||||
data-testid="empty-cart-message"
|
data-testid="empty-cart-message"
|
||||||
@@ -99,6 +117,9 @@ export default function Cart({
|
|||||||
</div>
|
</div>
|
||||||
{hasCartItems && (
|
{hasCartItems && (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex gap-x-4 px-4 pt-2 sm:justify-end sm:px-6 sm:pt-4">
|
||||||
|
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||||
|
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
||||||
<div className="flex gap-x-4 px-4 pt-2 sm:justify-end sm:px-6 sm:pt-4">
|
<div className="flex gap-x-4 px-4 pt-2 sm:justify-end sm:px-6 sm:pt-4">
|
||||||
<div className="w-full sm:mr-[42px] sm:w-auto">
|
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||||
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
||||||
@@ -106,6 +127,7 @@ export default function Cart({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||||
|
<p className="text-right text-sm">
|
||||||
<p className="text-right text-sm">
|
<p className="text-right text-sm">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: cart.subtotal,
|
value: cart.subtotal,
|
||||||
@@ -115,6 +137,9 @@ export default function Cart({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-x-4 px-4 py-2 sm:justify-end sm:px-6 sm:py-4">
|
||||||
|
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||||
|
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
||||||
<div className="flex gap-x-4 px-4 py-2 sm:justify-end sm:px-6 sm:py-4">
|
<div className="flex gap-x-4 px-4 py-2 sm:justify-end sm:px-6 sm:py-4">
|
||||||
<div className="w-full sm:mr-[42px] sm:w-auto">
|
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||||
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
<p className="text-muted-foreground ml-0 text-sm font-bold">
|
||||||
@@ -122,6 +147,7 @@ export default function Cart({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||||
|
<p className="text-right text-sm">
|
||||||
<p className="text-right text-sm">
|
<p className="text-right text-sm">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: cart.discount_total,
|
value: cart.discount_total,
|
||||||
@@ -131,6 +157,9 @@ export default function Cart({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-x-4 px-4 sm:justify-end sm:px-6">
|
||||||
|
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||||
|
<p className="ml-0 text-sm font-bold">
|
||||||
<div className="flex gap-x-4 px-4 sm:justify-end sm:px-6">
|
<div className="flex gap-x-4 px-4 sm:justify-end sm:px-6">
|
||||||
<div className="w-full sm:mr-[42px] sm:w-auto">
|
<div className="w-full sm:mr-[42px] sm:w-auto">
|
||||||
<p className="ml-0 text-sm font-bold">
|
<p className="ml-0 text-sm font-bold">
|
||||||
@@ -138,6 +167,7 @@ export default function Cart({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||||
|
<p className="text-right text-sm">
|
||||||
<p className="text-right text-sm">
|
<p className="text-right text-sm">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: cart.total,
|
value: cart.total,
|
||||||
@@ -150,8 +180,10 @@ export default function Cart({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-x-4 gap-y-6 py-4 sm:flex-row sm:py-8">
|
||||||
<div className="flex flex-col gap-x-4 gap-y-6 py-4 sm:flex-row sm:py-8">
|
<div className="flex flex-col gap-x-4 gap-y-6 py-4 sm:flex-row sm:py-8">
|
||||||
{IS_DISCOUNT_SHOWN && (
|
{IS_DISCOUNT_SHOWN && (
|
||||||
|
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
||||||
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
<h5>
|
<h5>
|
||||||
@@ -165,6 +197,7 @@ export default function Cart({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLocationsShown && (
|
{isLocationsShown && (
|
||||||
|
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
||||||
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
<Card className="flex w-full flex-col justify-between sm:w-1/2">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
<h5>
|
<h5>
|
||||||
@@ -176,12 +209,24 @@ export default function Cart({
|
|||||||
cart={{ ...cart }}
|
cart={{ ...cart }}
|
||||||
synlabAnalyses={synlabAnalyses}
|
synlabAnalyses={synlabAnalyses}
|
||||||
/>
|
/>
|
||||||
|
<AnalysisLocation
|
||||||
|
cart={{ ...cart }}
|
||||||
|
synlabAnalyses={synlabAnalyses}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<Button
|
||||||
|
className="h-10"
|
||||||
|
onClick={initiatePayment}
|
||||||
|
disabled={isInitiatingSession}
|
||||||
|
>
|
||||||
|
{isInitiatingSession && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="h-10"
|
className="h-10"
|
||||||
onClick={initiatePayment}
|
onClick={initiatePayment}
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ export interface MontonioOrderToken {
|
|||||||
| 'PENDING'
|
| 'PENDING'
|
||||||
| 'EXPIRED'
|
| 'EXPIRED'
|
||||||
| 'REFUNDED';
|
| 'REFUNDED';
|
||||||
|
| 'PAID'
|
||||||
|
| 'FAILED'
|
||||||
|
| 'CANCELLED'
|
||||||
|
| 'PENDING'
|
||||||
|
| 'EXPIRED'
|
||||||
|
| 'REFUNDED';
|
||||||
paymentMethod: string;
|
paymentMethod: string;
|
||||||
grandTotal: number;
|
grandTotal: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { JSX } from 'react';
|
import { JSX } from 'react';
|
||||||
|
|
||||||
|
import { StoreProduct } from '@medusajs/types';
|
||||||
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
||||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||||
import { Check, X } from 'lucide-react';
|
import { Check, X } from 'lucide-react';
|
||||||
|
|
||||||
|
import { PackageHeader } from '@kit/shared/components/package-header';
|
||||||
|
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||||
|
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -18,14 +22,14 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@kit/ui/table';
|
} from '@kit/ui/table';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { PackageHeader } from '@kit/shared/components/package-header';
|
|
||||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
|
||||||
import { StoreProduct } from '@medusajs/types';
|
|
||||||
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export type AnalysisPackageElement = Pick<StoreProduct, 'title' | 'id' | 'description'> & {
|
export type AnalysisPackageElement = Pick<
|
||||||
|
StoreProduct,
|
||||||
|
'title' | 'id' | 'description'
|
||||||
|
> & {
|
||||||
isIncludedInStandard: boolean;
|
isIncludedInStandard: boolean;
|
||||||
isIncludedInStandardPlus: boolean;
|
isIncludedInStandardPlus: boolean;
|
||||||
isIncludedInPremium: boolean;
|
isIncludedInPremium: boolean;
|
||||||
@@ -39,7 +43,11 @@ const CheckWithBackground = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PackageTableHead = async ({ product }: { product: AnalysisPackageWithVariant }) => {
|
const PackageTableHead = async ({
|
||||||
|
product,
|
||||||
|
}: {
|
||||||
|
product: AnalysisPackageWithVariant;
|
||||||
|
}) => {
|
||||||
const { t, language } = await createI18nServerInstance();
|
const { t, language } = await createI18nServerInstance();
|
||||||
|
|
||||||
const { title, price, nrOfAnalyses } = product;
|
const { title, price, nrOfAnalyses } = product;
|
||||||
@@ -48,14 +56,14 @@ const PackageTableHead = async ({ product }: { product: AnalysisPackageWithVaria
|
|||||||
<TableHead className="py-2">
|
<TableHead className="py-2">
|
||||||
<PackageHeader
|
<PackageHeader
|
||||||
title={t(title)}
|
title={t(title)}
|
||||||
tagColor='bg-cyan'
|
tagColor="bg-cyan"
|
||||||
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
|
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
|
||||||
language={language}
|
language={language}
|
||||||
price={price}
|
price={price}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ComparePackagesModal = async ({
|
const ComparePackagesModal = async ({
|
||||||
analysisPackages,
|
analysisPackages,
|
||||||
@@ -69,7 +77,9 @@ const ComparePackagesModal = async ({
|
|||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
const standardPackage = analysisPackages.find(({ isStandard }) => isStandard);
|
const standardPackage = analysisPackages.find(({ isStandard }) => isStandard);
|
||||||
const standardPlusPackage = analysisPackages.find(({ isStandardPlus }) => isStandardPlus);
|
const standardPlusPackage = analysisPackages.find(
|
||||||
|
({ isStandardPlus }) => isStandardPlus,
|
||||||
|
);
|
||||||
const premiumPackage = analysisPackages.find(({ isPremium }) => isPremium);
|
const premiumPackage = analysisPackages.find(({ isPremium }) => isPremium);
|
||||||
|
|
||||||
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
|
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
|
||||||
@@ -100,7 +110,7 @@ const ComparePackagesModal = async ({
|
|||||||
<p className="text-muted-foreground mx-auto w-3/5 text-sm">
|
<p className="text-muted-foreground mx-auto w-3/5 text-sm">
|
||||||
{t('product:healthPackageComparison.description')}
|
{t('product:healthPackageComparison.description')}
|
||||||
</p>
|
</p>
|
||||||
<div className="rounded-md border max-h-[80vh] overflow-y-auto">
|
<div className="max-h-[80vh] overflow-y-auto rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -112,16 +122,14 @@ const ComparePackagesModal = async ({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{analysisPackageElements.map(
|
{analysisPackageElements.map(
|
||||||
(
|
({
|
||||||
{
|
|
||||||
title,
|
title,
|
||||||
id,
|
id,
|
||||||
description,
|
description,
|
||||||
isIncludedInStandard,
|
isIncludedInStandard,
|
||||||
isIncludedInStandardPlus,
|
isIncludedInStandardPlus,
|
||||||
isIncludedInPremium,
|
isIncludedInPremium,
|
||||||
},
|
}) => {
|
||||||
) => {
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -130,20 +138,28 @@ const ComparePackagesModal = async ({
|
|||||||
<TableRow key={id}>
|
<TableRow key={id}>
|
||||||
<TableCell className="py-6 sm:max-w-[30vw]">
|
<TableCell className="py-6 sm:max-w-[30vw]">
|
||||||
{title}{' '}
|
{title}{' '}
|
||||||
{description && (<InfoTooltip content={description} icon={<QuestionMarkCircledIcon />} />)}
|
{description && (
|
||||||
|
<InfoTooltip
|
||||||
|
content={description}
|
||||||
|
icon={<QuestionMarkCircledIcon />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center" className="py-6">
|
<TableCell align="center" className="py-6">
|
||||||
{isIncludedInStandard && <CheckWithBackground />}
|
{isIncludedInStandard && <CheckWithBackground />}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center" className="py-6">
|
<TableCell align="center" className="py-6">
|
||||||
{isIncludedInStandardPlus && <CheckWithBackground />}
|
{isIncludedInStandardPlus && (
|
||||||
|
<CheckWithBackground />
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center" className="py-6">
|
<TableCell align="center" className="py-6">
|
||||||
{isIncludedInPremium && <CheckWithBackground />}
|
{isIncludedInPremium && <CheckWithBackground />}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function DashboardCards() {
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Card
|
<Card
|
||||||
variant="gradient-success"
|
variant="gradient-success"
|
||||||
className="xs:w-1/2 sm:w-auto flex w-full flex-col justify-between"
|
className="xs:w-1/2 flex w-full flex-col justify-between sm:w-auto"
|
||||||
>
|
>
|
||||||
<CardHeader className="flex-row">
|
<CardHeader className="flex-row">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,17 +3,11 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { BlendingModeIcon } from '@radix-ui/react-icons';
|
import { BlendingModeIcon } from '@radix-ui/react-icons';
|
||||||
import {
|
import { Droplets } from 'lucide-react';
|
||||||
Droplets,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import { Card, CardContent, CardHeader } from '@kit/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
} from '@kit/ui/card';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
|
||||||
import { Database } from '@/packages/supabase/src/database.types';
|
import { Database } from '@/packages/supabase/src/database.types';
|
||||||
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
||||||
|
import { isNil } from 'lodash';
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import type { AccountWithParams } from '@kit/accounts/types/accounts';
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -27,13 +28,13 @@ import {
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { isNil } from 'lodash';
|
|
||||||
import { BmiCategory } from '~/lib/types/bmi';
|
import { BmiCategory } from '~/lib/types/bmi';
|
||||||
import PersonalCode, {
|
import PersonalCode, {
|
||||||
bmiFromMetric,
|
bmiFromMetric,
|
||||||
getBmiBackgroundColor,
|
getBmiBackgroundColor,
|
||||||
getBmiStatus,
|
getBmiStatus,
|
||||||
} from '~/lib/utils';
|
} from '~/lib/utils';
|
||||||
|
|
||||||
import DashboardRecommendations from './dashboard-recommendations';
|
import DashboardRecommendations from './dashboard-recommendations';
|
||||||
|
|
||||||
const getCardVariant = (isSuccess: boolean | null): CardProps['variant'] => {
|
const getCardVariant = (isSuccess: boolean | null): CardProps['variant'] => {
|
||||||
@@ -113,15 +114,13 @@ const cards = ({
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
title: 'dashboard:smoking',
|
title: 'dashboard:smoking',
|
||||||
description:
|
description: isNil(smoking)
|
||||||
isNil(smoking)
|
|
||||||
? 'dashboard:respondToQuestion'
|
? 'dashboard:respondToQuestion'
|
||||||
: !!smoking
|
: smoking
|
||||||
? 'common:yes'
|
? 'common:yes'
|
||||||
: 'common:no',
|
: 'common:no',
|
||||||
descriptionColor: 'text-primary',
|
descriptionColor: 'text-primary',
|
||||||
icon:
|
icon: isNil(smoking) ? (
|
||||||
isNil(smoking) ? (
|
|
||||||
<Link href={pathsConfig.app.personalAccountSettings}>
|
<Link href={pathsConfig.app.personalAccountSettings}>
|
||||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||||
<ChevronRight className="size-4 stroke-2" />
|
<ChevronRight className="size-4 stroke-2" />
|
||||||
@@ -150,9 +149,11 @@ export default function Dashboard({
|
|||||||
let age: number = 0;
|
let age: number = 0;
|
||||||
let gender: { label: string; value: string } | null = null;
|
let gender: { label: string; value: string } | null = null;
|
||||||
try {
|
try {
|
||||||
({ age = 0, gender } = PersonalCode.parsePersonalCode(account.personal_code!));
|
({ age = 0, gender } = PersonalCode.parsePersonalCode(
|
||||||
|
account.personal_code!,
|
||||||
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse personal code", e);
|
console.error('Failed to parse personal code', e);
|
||||||
}
|
}
|
||||||
const bmiStatus = getBmiStatus(bmiThresholds, { age, height, weight });
|
const bmiStatus = getBmiStatus(bmiThresholds, { age, height, weight });
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import { useContext } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||||
|
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||||
import { SidebarContext } from '@kit/ui/shadcn-sidebar';
|
import { SidebarContext } from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
import { pathsConfig, featureFlagsConfig } from '@kit/shared/config';
|
|
||||||
|
|
||||||
|
|
||||||
const features = {
|
const features = {
|
||||||
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
|
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export function HomeLayoutPageHeader(
|
|||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<PageHeader description={props.description} title={props.title}>{props.children}</PageHeader>
|
<PageHeader description={props.description} title={props.title}>
|
||||||
|
{props.children}
|
||||||
|
</PageHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||||
|
import { StoreProduct } from '@medusajs/types';
|
||||||
import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react';
|
import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardDescription,
|
|
||||||
} from '@kit/ui/card';
|
|
||||||
import { StoreProduct } from '@medusajs/types';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
|
||||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { Card, CardDescription, CardFooter, CardHeader } from '@kit/ui/card';
|
||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
||||||
|
|
||||||
export type OrderAnalysisCard = Pick<
|
export type OrderAnalysisCard = Pick<
|
||||||
StoreProduct, 'title' | 'description' | 'subtitle'
|
StoreProduct,
|
||||||
|
'title' | 'description' | 'subtitle'
|
||||||
> & {
|
> & {
|
||||||
variant: { id: string };
|
variant: { id: string };
|
||||||
price: number | null;
|
price: number | null;
|
||||||
@@ -32,10 +30,13 @@ export default function OrderAnalysesCards({
|
|||||||
analyses: OrderAnalysisCard[];
|
analyses: OrderAnalysisCard[];
|
||||||
countryCode: string;
|
countryCode: string;
|
||||||
}) {
|
}) {
|
||||||
|
const {
|
||||||
|
i18n: { language },
|
||||||
|
} = useTranslation();
|
||||||
|
|
||||||
const { i18n: { language } } = useTranslation()
|
const [variantAddingToCart, setVariantAddingToCart] = useState<string | null>(
|
||||||
|
null,
|
||||||
const [variantAddingToCart, setVariantAddingToCart] = useState<string | null>(null);
|
);
|
||||||
const handleSelect = async (variantId: string) => {
|
const handleSelect = async (variantId: string) => {
|
||||||
if (variantAddingToCart) {
|
if (variantAddingToCart) {
|
||||||
return null;
|
return null;
|
||||||
@@ -54,18 +55,13 @@ export default function OrderAnalysesCards({
|
|||||||
setVariantAddingToCart(null);
|
setVariantAddingToCart(null);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid xs:grid-cols-3 gap-6 mt-4">
|
<div className="xs:grid-cols-3 mt-4 grid gap-6">
|
||||||
{analyses.map(({
|
{analyses.map(({ title, variant, description, subtitle, price }) => {
|
||||||
title,
|
const formattedPrice =
|
||||||
variant,
|
typeof price === 'number'
|
||||||
description,
|
|
||||||
subtitle,
|
|
||||||
price,
|
|
||||||
}) => {
|
|
||||||
const formattedPrice = typeof price === 'number'
|
|
||||||
? formatCurrency({
|
? formatCurrency({
|
||||||
currencyCode: 'eur',
|
currencyCode: 'eur',
|
||||||
locale: language,
|
locale: language,
|
||||||
@@ -80,30 +76,36 @@ export default function OrderAnalysesCards({
|
|||||||
>
|
>
|
||||||
<CardHeader className="flex-row">
|
<CardHeader className="flex-row">
|
||||||
<div
|
<div
|
||||||
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
className={
|
||||||
|
'bg-primary/10 mb-6 flex size-8 items-center-safe justify-center-safe rounded-full text-white'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<HeartPulse className="size-4 fill-green-500" />
|
<HeartPulse className="size-4 fill-green-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
<div className="bg-warning ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white">
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="px-2 text-black"
|
className="px-2 text-black"
|
||||||
onClick={() => handleSelect(variant.id)}
|
onClick={() => handleSelect(variant.id)}
|
||||||
>
|
>
|
||||||
{variantAddingToCart === variant.id ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
{variantAddingToCart === variant.id ? (
|
||||||
|
<Loader2 className="size-4 animate-spin stroke-2" />
|
||||||
|
) : (
|
||||||
|
<ShoppingCart className="size-4 stroke-2" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex gap-2">
|
<CardFooter className="flex gap-2">
|
||||||
<div className="flex flex-col items-start gap-2 flex-1">
|
<div className="flex flex-1 flex-col items-start gap-2">
|
||||||
<h5>
|
<h5>
|
||||||
{title}
|
{title}
|
||||||
{description && (
|
{description && (
|
||||||
<>
|
<>
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
content={
|
content={
|
||||||
<div className='flex flex-col gap-2'>
|
<div className="flex flex-col gap-2">
|
||||||
<span>{formattedPrice}</span>
|
<span>{formattedPrice}</span>
|
||||||
<span>{description}</span>
|
<span>{description}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,11 +114,7 @@ export default function OrderAnalysesCards({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
{subtitle && (
|
{subtitle && <CardDescription>{subtitle}</CardDescription>}
|
||||||
<CardDescription>
|
|
||||||
{subtitle}
|
|
||||||
</CardDescription>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2 self-end text-sm">
|
<div className="flex flex-col items-end gap-2 self-end text-sm">
|
||||||
<span>{formattedPrice}</span>
|
<span>{formattedPrice}</span>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user