20 Commits

Author SHA1 Message Date
615dde52e6 wip4 2025-07-10 14:52:18 +03:00
7f2c6f2374 wip4 2025-07-10 14:45:41 +03:00
6368a5b5ff wip3 2025-07-10 14:44:39 +03:00
8e82736f09 wip2 2025-07-10 14:44:06 +03:00
7b71da3825 feat(MED-122): use <Trans> instead of t() 2025-07-10 13:24:03 +03:00
ad213dd4f8 feat(MED-122): consistently format name in titlecase 2025-07-10 13:19:28 +03:00
0a0b1f0dee feat(MED-122): update current user account loader 2025-07-10 13:19:01 +03:00
bbcf0b6d83 feat(MED-122): update analysis packages page, pageheader 2025-07-10 11:36:57 +03:00
danelkungla
0af3823148 FIX Build
Fix build
2025-07-09 14:02:53 +03:00
Danel Kungla
23b54bb4f4 Merge branch 'main' into FIX-BUILD 2025-07-09 14:02:10 +03:00
Danel Kungla
c5ddccc15d fix build 2025-07-09 14:01:43 +03:00
danelkungla
023bc897c2 MED-63: change api to medreport schema
MED-63
* membership confirmation flow
* update schema public -> medreport
2025-07-09 13:40:28 +03:00
Danel Kungla
d9198a8a12 add medreport schema 2025-07-09 13:31:37 +03:00
Danel Kungla
0b8fadb771 remove medreport product migration and related constraints 2025-07-09 10:01:39 +03:00
Danel Kungla
9371ff7710 feat: update API and database structure for membership confirmation and wallet integration
- Refactor API calls to use 'medreport' schema for membership confirmation and account updates
- Enhance success notification component to include wallet balance and expiration details
- Modify database types to reflect new 'medreport' schema and add product-related tables
- Update localization files to support new wallet messages
- Adjust SQL migration scripts for proper schema references and function definitions
2025-07-09 10:01:12 +03:00
Danel Kungla
4f36f9c037 refactor: clean up imports and enhance error logging in user workspace and team invitations actions 2025-07-08 18:34:21 +03:00
Danel Kungla
29ff8cb512 fix pnpm-lock 2025-07-08 16:06:39 +03:00
Danel Kungla
c0a5238e19 Merge branch 'main' into MED-63 2025-07-08 16:06:27 +03:00
danelkungla
7bf5dd8899 Merge pull request #31 from MR-medreport/B2B-26
B2B-26: move selfservice tables to medreport schema and add medusa store
2025-07-08 15:56:30 +03:00
Danel Kungla
2e62e4b0eb move selfservice tables to medreport schema
add base medusa store frontend
2025-07-07 13:46:22 +03:00
341 changed files with 26478 additions and 1099 deletions

5
.env
View File

@@ -51,4 +51,7 @@ LOGGER=pino
NEXT_PUBLIC_DEFAULT_LOCALE=et
NEXT_PUBLIC_TEAM_NAVIGATION_STYLE=custom
NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom
NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom
# MEDUSA
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=

View File

@@ -16,4 +16,6 @@ EMAIL_USER= # refer to your email provider's documentation
EMAIL_PASSWORD= # refer to your email provider's documentation
EMAIL_HOST= # refer to your email provider's documentation
EMAIL_PORT= # or 465 for SSL
EMAIL_TLS= # or false for SSL (see provider documentation)
EMAIL_TLS= # or false for SSL (see provider documentation)
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=

View File

@@ -39,6 +39,8 @@ pnpm clean
pnpm i
```
if you get missing dependency error do `pnpm i --force`
## Adding new dependency
```bash
@@ -71,6 +73,12 @@ To update database types run:
npm run supabase:typegen:app
```
## Medusa store
To get medusa store working you need to update the env's to your running medusa app and migrate the tables from medusa project to your supabase project
You can get `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` from your medusa app settings
## Super admin
To access admin pages follow these steps:

View File

@@ -0,0 +1,89 @@
'use client';
import { useRouter } from 'next/navigation';
import { SubmitButton } from '@/components/ui/submit-button';
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { CompanySubmitData } from '@/lib/types/company';
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FormItem } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
const CompanyOfferForm = () => {
const router = useRouter();
const language = useTranslation().i18n.language;
const {
register,
handleSubmit,
formState: { isValid, isSubmitting },
} = useForm({
resolver: zodResolver(companyOfferSchema),
mode: 'onChange',
});
const onSubmit = async (data: CompanySubmitData) => {
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
if (value !== undefined) formData.append(key, value);
});
try {
sendCompanyOfferEmail(data, language)
.then(() => router.push('/company-offer/success'))
.catch((error) => alert('error: ' + error));
} catch (err: unknown) {
if (err instanceof Error) {
console.warn('Server validation error: ' + err.message);
}
console.warn('Server validation error: ', err);
}
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
noValidate
className="flex flex-col gap-6 px-6 pt-8 text-left"
>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:companyName'} />
</Label>
<Input {...register('companyName')} />
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:contactPerson'} />
</Label>
<Input {...register('contactPerson')} />
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:email'} />
</Label>
<Input type="email" {...register('email')}></Input>
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:phone'} />
</Label>
<Input type="tel" {...register('phone')} />
</FormItem>
<SubmitButton
disabled={!isValid || isSubmitting}
pendingText="Saatmine..."
type="submit"
>
<Trans i18nKey={'account:requestCompanyAccount:button'} />
</SubmitButton>
</form>
);
};
export default CompanyOfferForm;

View File

@@ -1,53 +1,13 @@
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { MedReportLogo } from '@/components/med-report-logo';
import { SubmitButton } from '@/components/ui/submit-button';
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { CompanySubmitData } from '@/lib/types/company';
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { withI18n } from '@/lib/i18n/with-i18n';
import { FormItem } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
export default function CompanyOffer() {
const router = useRouter();
const {
register,
handleSubmit,
formState: { isValid, isSubmitting },
} = useForm({
resolver: zodResolver(companyOfferSchema),
mode: 'onChange',
});
const language = useTranslation().i18n.language;
const onSubmit = async (data: CompanySubmitData) => {
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
if (value !== undefined) formData.append(key, value);
});
try {
sendCompanyOfferEmail(data, language)
.then(() => router.push('/company-offer/success'))
.catch((error) => alert('error: ' + error));
} catch (err: unknown) {
if (err instanceof Error) {
console.warn('Server validation error: ' + err.message);
}
console.warn('Server validation error: ', err);
}
};
import CompanyOfferForm from './_components/company-offer-form';
function CompanyOffer() {
return (
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
<div className="flex w-1/2 flex-col px-12 py-14 text-center">
@@ -58,45 +18,11 @@ export default function CompanyOffer() {
<p className="text-muted-foreground pt-2 text-sm">
<Trans i18nKey={'account:requestCompanyAccount:description'} />
</p>
<form
onSubmit={handleSubmit(onSubmit)}
noValidate
className="flex flex-col gap-6 px-6 pt-8 text-left"
>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:companyName'} />
</Label>
<Input {...register('companyName')} />
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:contactPerson'} />
</Label>
<Input {...register('contactPerson')} />
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:email'} />
</Label>
<Input type="email" {...register('email')}></Input>
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:phone'} />
</Label>
<Input type="tel" {...register('phone')} />
</FormItem>
<SubmitButton
disabled={!isValid || isSubmitting}
pendingText="Saatmine..."
type="submit"
>
<Trans i18nKey={'account:requestCompanyAccount:button'} />
</SubmitButton>
</form>
<CompanyOfferForm />
</div>
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"></div>
</div>
);
}
export default withI18n(CompanyOffer);

View File

@@ -34,6 +34,7 @@ async function accountLoader(id: string) {
const client = getSupabaseServerClient();
const { data, error } = await client
.schema('medreport')
.from('accounts')
.select('*, memberships: accounts_memberships (*)')
.eq('id', id)

View File

@@ -0,0 +1,34 @@
'use client';
import React from 'react';
import pathsConfig from '@/config/paths.config';
import { useTranslation } from 'react-i18next';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { SuccessNotification } from '@kit/notifications/components';
const MembershipConfirmationNotification: React.FC<{
userId: string;
}> = ({ userId }) => {
const { t } = useTranslation();
const { data: accountData } = usePersonalAccountData(userId);
return (
<SuccessNotification
showLogo={false}
title={t('account:membershipConfirmation:successTitle', {
firstName: accountData?.name,
lastName: accountData?.last_name,
})}
descriptionKey="account:membershipConfirmation:successDescription"
buttonProps={{
buttonTitleKey: 'account:membershipConfirmation:successButton',
href: pathsConfig.app.selectPackage,
}}
/>
);
};
export default MembershipConfirmationNotification;

View File

@@ -8,4 +8,4 @@ async function SiteLayout(props: React.PropsWithChildren) {
);
}
export default withI18n(SiteLayout);
export default SiteLayout;

View File

@@ -1,16 +1,14 @@
import { redirect } from 'next/navigation';
import pathsConfig from '@/config/paths.config';
import { useTranslation } from 'react-i18next';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { SuccessNotification } from '@kit/notifications/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { withI18n } from '~/lib/i18n/with-i18n';
import MembershipConfirmationNotification from './_components/membership-confirmation-notification';
async function UpdateAccountSuccess() {
const { t } = useTranslation('account');
const client = getSupabaseServerClient();
const {
@@ -21,26 +19,7 @@ async function UpdateAccountSuccess() {
redirect(pathsConfig.app.home);
}
const { data: accountData } = usePersonalAccountData(user.id);
if (!accountData?.id) {
redirect(pathsConfig.app.home);
}
return (
<SuccessNotification
showLogo={false}
title={t('account:membershipConfirmation:successTitle', {
firstName: accountData?.name,
lastName: accountData?.last_name,
})}
descriptionKey="account:membershipConfirmation:successDescription"
buttonProps={{
buttonTitleKey: 'account:membershipConfirmation:successButton',
href: pathsConfig.app.selectPackage,
}}
/>
);
return <MembershipConfirmationNotification userId={user.id} />;
}
export default withI18n(UpdateAccountSuccess);

View File

@@ -26,7 +26,7 @@ async function getSupabaseHealthCheck() {
try {
const client = getSupabaseServerAdminClient();
const { error } = await client.rpc('is_set', {
const { error } = await client.schema('medreport').rpc('is_set', {
field_name: 'billing_provider',
});

View File

@@ -0,0 +1,34 @@
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
import OrderCards from '../../_components/order-cards';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('booking:title');
return {
title,
};
};
function BookingPage() {
return (
<>
<HomeLayoutPageHeader
title={<Trans i18nKey={'booking:title'} />}
description={<Trans i18nKey={'booking:description'} />}
/>
<PageBody>
<OrderCards />
</PageBody>
</>
);
}
export default withI18n(BookingPage);

View File

@@ -0,0 +1,5 @@
import SkeletonCartPage from '~/medusa/modules/skeletons/templates/skeleton-cart-page';
export default function Loading() {
return <SkeletonCartPage />;
}

View File

@@ -0,0 +1,21 @@
import { Metadata } from 'next';
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
export const metadata: Metadata = {
title: '404',
description: 'Something went wrong',
};
export default function NotFound() {
return (
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center">
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
<p className="text-small-regular text-ui-fg-base">
The cart you tried to access does not exist. Clear your cookies and try
again.
</p>
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveCart } from '~/medusa/lib/data/cart';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import CartTemplate from '~/medusa/modules/cart/templates';
export const metadata: Metadata = {
title: 'Cart',
description: 'View your cart',
};
export default async function Cart() {
const cart2 = await retrieveCart().catch((error) => {
console.error(error);
return notFound();
});
const customer = await retrieveCustomer();
const cart: NonNullable<typeof cart2> = {
items: [
{
id: '1',
quantity: 1,
cart: cart2!,
item_total: 100,
item_subtotal: 100,
item_tax_total: 100,
original_total: 100,
original_subtotal: 100,
original_tax_total: 100,
total: 100,
subtotal: 100,
tax_total: 100,
title: 'Test',
requires_shipping: true,
discount_total: 0,
discount_tax_total: 0,
metadata: {},
created_at: new Date(),
is_discountable: true,
is_tax_inclusive: true,
unit_price: 100,
cart_id: '1',
},
],
}
return (
<PageBody>
<PageHeader title={`Ostukorv`} description={`Vali kalendrist sobiv kuupäev ja broneeri endale vastuvõtuaeg.`} />
<CartTemplate cart={cart} customer={customer} />
</PageBody>
);
}

View File

@@ -0,0 +1,42 @@
import { Scale } from 'lucide-react';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Button } from '@kit/ui/button';
import SelectAnalysisPackages from '@/components/select-analysis-packages';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import ComparePackagesModal from '../../_components/compare-packages-modal';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('order-analysis-package:title');
return {
title,
};
};
async function OrderAnalysisPackagePage() {
return (
<PageBody>
<div className="space-y-3 text-center">
<h3>
<Trans i18nKey={'marketing:selectPackage'} />
</h3>
<ComparePackagesModal
triggerElement={
<Button variant="secondary" className="gap-2">
<Trans i18nKey={'marketing:comparePackages'} />
<Scale className="size-4 stroke-[1.5px]" />
</Button>
}
/>
</div>
<SelectAnalysisPackages />
</PageBody>
);
}
export default withI18n(OrderAnalysisPackagePage);

View File

@@ -1,14 +1,13 @@
import { Trans } from '@kit/ui/trans';
import { redirect } from 'next/navigation';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { toTitleCase } from '@/lib/utils';
import Dashboard from '../_components/dashboard';
// local imports
import { HomeLayoutPageHeader } from '../_components/home-page-header';
import { use } from 'react';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
import { PageBody } from '@kit/ui/page';
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
@@ -19,15 +18,24 @@ export const generateMetadata = async () => {
};
};
function UserHomePage() {
const { tempVisibleAccounts } = use(loadUserWorkspace());
async function UserHomePage() {
const account = await loadCurrentUserAccount();
if (!account) {
redirect('/');
}
return (
<>
<HomeLayoutPageHeader
title={<Trans i18nKey={'common:routes.home'} />}
description={<></>}
<PageHeader title={
<>
<Trans i18nKey={'common:welcome'} />
{account.name ? `, ${toTitleCase(account.name)}` : ''}
</>
}
description={
<Trans i18nKey={'dashboard:recentlyCheckedDescription'} />
}
/>
<PageBody>
<Dashboard />
</PageBody>

View File

@@ -18,12 +18,10 @@ import {
TableHeader,
TableRow,
} from '@kit/ui/table';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { PackageHeader } from '../../../../components/package-header';
import { InfoTooltip } from '../../../../components/ui/info-tooltip';
import { PackageHeader } from '@/components/package-header';
import { InfoTooltip } from '@/components/ui/info-tooltip';
const dummyCards = [
{

View File

@@ -1,7 +1,7 @@
'use client';
import Link from 'next/link';
import { InfoTooltip } from '@/components/ui/info-tooltip';
import { toTitleCase } from '@/lib/utils';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import {
Activity,
@@ -15,8 +15,6 @@ import {
User,
} from 'lucide-react';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
import { Button } from '@kit/ui/button';
import {
Card,
@@ -26,7 +24,6 @@ import {
CardHeader,
CardProps,
} from '@kit/ui/card';
import { PageDescription } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
@@ -107,6 +104,7 @@ const dummyRecommendations = [
tooltipContent: 'Selgitus',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
{
icon: <BlendingModeIcon className="size-4" />,
@@ -115,6 +113,7 @@ const dummyRecommendations = [
tooltipContent: 'Selgitus',
description: 'LDL-Kolesterool',
buttonText: 'Broneeri',
href: '/home/booking',
},
{
icon: <Droplets />,
@@ -124,24 +123,13 @@ const dummyRecommendations = [
description: 'Score-Risk 2',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
];
export default function Dashboard() {
const userWorkspace = useUserWorkspace();
const account = usePersonalAccountData(userWorkspace.user.id);
return (
<>
<div>
<h4>
<Trans i18nKey={'common:welcome'} />
{account?.data?.name ? `, ${toTitleCase(account.data.name)}` : ''}
</h4>
<PageDescription>
<Trans i18nKey={'dashboard:recentlyCheckedDescription'} />:
</PageDescription>
</div>
<div className="grid auto-rows-fr grid-cols-5 gap-3">
{dummyCards.map(
({
@@ -196,6 +184,7 @@ export default function Dashboard() {
tooltipContent,
price,
buttonText,
href,
},
index,
) => {
@@ -222,9 +211,17 @@ export default function Dashboard() {
</div>
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4">
<p className="text-sm font-medium"> {price}</p>
<Button size="sm" variant="secondary">
{buttonText}
</Button>
{href ? (
<Link href={href}>
<Button size="sm" variant="secondary">
{buttonText}
</Button>
</Link>
) : (
<Button size="sm" variant="secondary">
{buttonText}
</Button>
)}
</div>
</div>
);

View File

@@ -1,16 +1,15 @@
import Link from 'next/link';
import { ShoppingCart } from 'lucide-react';
import { Trans } from '@kit/ui/trans';
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
import { Search } from '~/components/ui/search';
import { SIDEBAR_WIDTH_PROPERTY } from '@/packages/ui/src/shadcn/constants';
import { Button } from '@kit/ui/button';
import { SIDEBAR_WIDTH_PROPERTY } from '../../../../packages/ui/src/shadcn/constants';
// home imports
import { UserNotifications } from '../_components/user-notifications';
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
import { Button } from '@kit/ui/button';
import { ShoppingCart } from 'lucide-react';
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
const { workspace, user, accounts } = props.workspace;
@@ -30,10 +29,12 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' variant='ghost'>
<span className='flex items-center text-nowrap'> 231,89</span>
</Button>
<Button variant="ghost" className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' >
<ShoppingCart className="stroke-[1.5px]" />
<Trans i18nKey="common:shoppingCart" /> (0)
</Button>
<Link href='/home/cart'>
<Button variant="ghost" className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' >
<ShoppingCart className="stroke-[1.5px]" />
<Trans i18nKey="common:shoppingCart" /> (0)
</Button>
</Link>
<UserNotifications userId={user.id} />
<div>

View File

@@ -7,6 +7,6 @@ export function HomeLayoutPageHeader(
}>,
) {
return (
<PageHeader description={props.description}>{props.children}</PageHeader>
<PageHeader description={props.description} title={props.title}>{props.children}</PageHeader>
);
}

View File

@@ -0,0 +1,77 @@
"use client";
import { ChevronRight, HeartPulse } from 'lucide-react';
import Link from 'next/link';
import { Button } from '@kit/ui/button';
import {
Card,
CardHeader,
CardDescription,
CardProps,
CardFooter,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@/lib/utils';
const dummyCards = [
{
title: 'booking:analysisPackages.title',
description: 'booking:analysisPackages.description',
descriptionColor: 'text-primary',
icon: (
<Link href={'/home/order-analysis-package'}>
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
),
cardVariant: 'gradient-success' as CardProps['variant'],
iconBg: 'bg-warning',
},
];
export default function OrderCards() {
return (
<div className="grid grid-cols-3 gap-6 mt-4">
{dummyCards.map(({
title,
description,
icon,
cardVariant,
descriptionColor,
iconBg,
}) => (
<Card
key={title}
variant={cardVariant}
className="flex flex-col justify-between"
>
<CardHeader className="items-end-safe">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
iconBg,
)}
>
{icon}
</div>
</CardHeader>
<CardFooter className="flex flex-col items-start gap-2">
<div
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
>
<HeartPulse className="size-4 fill-green-500" />
</div>
<h5>
<Trans i18nKey={title} />
</h5>
<CardDescription className={descriptionColor}>
<Trans i18nKey={description} />
</CardDescription>
</CardFooter>
</Card>
))}
</div>
);
}

View File

@@ -2,6 +2,7 @@ import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
export type UserAccount = Awaited<ReturnType<typeof loadUserAccount>>;
@@ -13,6 +14,13 @@ export type UserAccount = Awaited<ReturnType<typeof loadUserAccount>>;
*/
export const loadUserAccount = cache(accountLoader);
export async function loadCurrentUserAccount() {
const user = await requireUserInServerComponent();
return user?.identities?.[0]?.id
? await loadUserAccount(user?.identities?.[0]?.id)
: null;
}
async function accountLoader(accountId: string) {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);

View File

@@ -35,13 +35,13 @@ async function workspaceLoader() {
accountsPromise(),
workspacePromise,
requireUserInServerComponent(),
tempAccountsPromise()
tempAccountsPromise(),
]);
return {
accounts,
workspace,
user,
tempVisibleAccounts
tempVisibleAccounts,
};
}

View File

@@ -46,9 +46,11 @@ async function loadAccountMembers(
client: SupabaseClient<Database>,
account: string,
) {
const { data, error } = await client.rpc('get_account_members', {
account_slug: account,
});
const { data, error } = await client
.schema('medreport')
.rpc('get_account_members', {
account_slug: account,
});
if (error) {
console.error(error);
@@ -67,9 +69,11 @@ async function loadInvitations(
client: SupabaseClient<Database>,
account: string,
) {
const { data, error } = await client.rpc('get_account_invitations', {
account_slug: account,
});
const { data, error } = await client
.schema('medreport')
.rpc('get_account_invitations', {
account_slug: account,
});
if (error) {
console.error(error);

View File

@@ -1,6 +1,6 @@
import { requireUserInServerComponent } from '../../lib/server/require-user-in-server-component';
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
import ConsentDialog from './(user)/_components/consent-dialog';
import { loadUserAccount } from './(user)/_lib/server/load-user-account';
import { loadCurrentUserAccount } from './(user)/_lib/server/load-user-account';
export default async function HomeLayout({
children,
@@ -8,9 +8,7 @@ export default async function HomeLayout({
children: React.ReactNode;
}) {
const user = await requireUserInServerComponent();
const account = user?.identities?.[0]?.id
? await loadUserAccount(user?.identities?.[0]?.id)
: null;
const account = await loadCurrentUserAccount()
if (account && account?.has_consent_anonymized_company_statistics === null) {
return (

View File

@@ -68,23 +68,20 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
const invitation = await api.getInvitation(adminClient, token);
// the invitation is not found or expired
if (!invitation) {
return (
<AuthLayoutShell Logo={AppLogo}>
<InviteNotFoundOrExpired />
</AuthLayoutShell>
);
}
// we need to verify the user isn't already in the account
// we do so by checking if the user can read the account
// if the user can read the account, then they are already in the account
const { data: isAlreadyTeamMember } = await client.rpc(
'is_account_team_member',
{
const { data: isAlreadyTeamMember } = await client
.schema('medreport')
.rpc('is_account_team_member', {
target_account_id: invitation.account.id,
},
);
});
// if the user is already in the account redirect to the home page
if (isAlreadyTeamMember) {

View File

@@ -1,24 +1,16 @@
import Image from 'next/image';
import Link from 'next/link';
import { CaretRightIcon } from '@radix-ui/react-icons';
import { Scale } from 'lucide-react';
import { Trans } from '@kit/ui/trans';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
} from '@kit/ui/card';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import SelectAnalysisPackages from '@/components/select-analysis-packages';
import { MedReportLogo } from '../../components/med-report-logo';
import { PackageHeader } from '../../components/package-header';
import { ButtonTooltip } from '../../components/ui/button-tooltip';
import pathsConfig from '../../config/paths.config';
import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal';
@@ -30,100 +22,30 @@ export const generateMetadata = async () => {
};
};
const dummyCards = [
{
titleKey: 'product:standard.label',
price: 40,
nrOfAnalyses: 4,
tagColor: 'bg-cyan',
descriptionKey: 'marketing:standard.description',
},
{
titleKey: 'product:standardPlus.label',
price: 85,
nrOfAnalyses: 10,
tagColor: 'bg-warning',
descriptionKey: 'product:standardPlus.description',
},
{
titleKey: 'product:premium.label',
price: 140,
nrOfAnalyses: '12+',
tagColor: 'bg-purple',
descriptionKey: 'product:premium.description',
},
];
async function SelectPackagePage() {
const { t, language } = await createI18nServerInstance();
return (
<div className="container mx-auto my-24 flex flex-col items-center space-y-12">
<MedReportLogo />
<div className="space-y-3 text-center">
<h3>{t('marketing:selectPackage')}</h3>
<h3>
<Trans i18nKey={'marketing:selectPackage'} />
</h3>
<ComparePackagesModal
triggerElement={
<Button variant="secondary" className="gap-2">
{t('marketing:comparePackages')}
<Trans i18nKey={'marketing:comparePackages'} />
<Scale className="size-4 stroke-[1.5px]" />
</Button>
}
/>
</div>
<div className="grid grid-cols-3 gap-6">
{dummyCards.map(
(
{ titleKey, price, nrOfAnalyses, tagColor, descriptionKey },
index,
) => {
return (
<Card key={index}>
<CardHeader className="relative">
<ButtonTooltip
content="Content pending"
className="absolute top-5 right-5 z-10"
/>
<Image
src="/assets/card-image.png"
alt="background"
width={326}
height={195}
className="max-h-48 w-full opacity-10"
/>
</CardHeader>
<CardContent className="space-y-1 text-center">
<PackageHeader
title={t(titleKey)}
tagColor={tagColor}
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
language={language}
price={price}
/>
<CardDescription>{t(descriptionKey)}</CardDescription>
</CardContent>
<CardFooter>
<Button className="w-full">
{t('marketing:selectThisPackage')}
</Button>
</CardFooter>
</Card>
);
},
)}
<div className="col-span-3 grid grid-cols-subgrid">
<div className="col-start-2 justify-self-center-safe">
<Link href={pathsConfig.app.home}>
<Button variant="secondary" className="align-center">
{t('marketing:notInterestedInAudit')}{' '}
<CaretRightIcon className="size-4" />
</Button>
</Link>
</div>
</div>
</div>
<SelectAnalysisPackages />
<Link href={pathsConfig.app.home}>
<Button variant="secondary" className="align-center">
<Trans i18nKey={'marketing:notInterestedInAudit'} />{' '}
<CaretRightIcon className="size-4" />
</Button>
</Link>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import { retrieveCart } from "@lib/data/cart"
import { retrieveCustomer } from "@lib/data/customer"
import PaymentWrapper from "@modules/checkout/components/payment-wrapper"
import CheckoutForm from "@modules/checkout/templates/checkout-form"
import CheckoutSummary from "@modules/checkout/templates/checkout-summary"
import { Metadata } from "next"
import { notFound } from "next/navigation"
export const metadata: Metadata = {
title: "Checkout",
}
export default async function Checkout() {
const cart = await retrieveCart()
if (!cart) {
return notFound()
}
const customer = await retrieveCustomer()
return (
<div className="grid grid-cols-1 small:grid-cols-[1fr_416px] content-container gap-x-40 py-12">
<PaymentWrapper cart={cart}>
<CheckoutForm cart={cart} customer={customer} />
</PaymentWrapper>
<CheckoutSummary cart={cart} />
</div>
)
}

View File

@@ -0,0 +1,45 @@
import { LocalizedClientLink } from '~/medusa/modules/common/components';
import { ChevronDownIcon } from '~/medusa/modules/common/icons';
import { MedusaCTA } from '~/medusa/modules/layout/components';
export default function CheckoutLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="small:min-h-screen relative w-full bg-white">
<div className="h-16 border-b bg-white">
<nav className="content-container flex h-full items-center justify-between">
<LocalizedClientLink
href="/cart"
className="text-small-semi text-ui-fg-base flex flex-1 basis-0 items-center gap-x-2 uppercase"
data-testid="back-to-cart-link"
>
<ChevronDownIcon className="rotate-90" size={16} />
<span className="small:block txt-compact-plus text-ui-fg-subtle hover:text-ui-fg-base mt-px hidden">
Back to shopping cart
</span>
<span className="small:hidden txt-compact-plus text-ui-fg-subtle hover:text-ui-fg-base mt-px block">
Back
</span>
</LocalizedClientLink>
<LocalizedClientLink
href="/"
className="txt-compact-xlarge-plus text-ui-fg-subtle hover:text-ui-fg-base uppercase"
data-testid="store-link"
>
Medusa Store
</LocalizedClientLink>
<div className="flex-1 basis-0" />
</nav>
</div>
<div className="relative" data-testid="checkout-container">
{children}
</div>
<div className="flex w-full items-center justify-center py-4">
<MedusaCTA />
</div>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { Metadata } from 'next';
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
export const metadata: Metadata = {
title: '404',
description: 'Something went wrong',
};
export default async function NotFound() {
return (
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
<p className="text-small-regular text-ui-fg-base">
The page you tried to access does not exist.
</p>
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import { getRegion } from '~/medusa/lib/data/regions';
import AddressBook from '~/medusa/modules/account/components/address-book';
export const metadata: Metadata = {
title: 'Addresses',
description: 'View your addresses',
};
export default async function Addresses(props: {
params: Promise<{ countryCode: string }>;
}) {
const params = await props.params;
const { countryCode } = params;
const customer = await retrieveCustomer();
const region = await getRegion(countryCode);
if (!customer || !region) {
notFound();
}
return (
<div className="w-full" data-testid="addresses-page-wrapper">
<div className="mb-8 flex flex-col gap-y-4">
<h1 className="text-2xl-semi">Shipping Addresses</h1>
<p className="text-base-regular">
View and update your shipping addresses, you can add as many as you
like. Saving your addresses will make them available during checkout.
</p>
</div>
<AddressBook customer={customer} region={region} />
</div>
);
}

View File

@@ -0,0 +1,9 @@
import Spinner from '~/medusa/modules/common/icons/spinner';
export default function Loading() {
return (
<div className="text-ui-fg-base flex h-full w-full items-center justify-center">
<Spinner size={36} />
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveOrder } from '~/medusa/lib/data/orders';
import OrderDetailsTemplate from '~/medusa/modules/order/templates/order-details-template';
type Props = {
params: Promise<{ id: string }>;
};
export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params;
const order = await retrieveOrder(params.id).catch(() => null);
if (!order) {
notFound();
}
return {
title: `Order #${order.display_id}`,
description: `View your order`,
};
}
export default async function OrderDetailPage(props: Props) {
const params = await props.params;
const order = await retrieveOrder(params.id).catch(() => null);
if (!order) {
notFound();
}
return <OrderDetailsTemplate order={order} />;
}

View File

@@ -0,0 +1,38 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { listOrders } from '~/medusa/lib/data/orders';
import OrderOverview from '~/medusa/modules/account/components/order-overview';
import TransferRequestForm from '~/medusa/modules/account/components/transfer-request-form';
import Divider from '~/medusa/modules/common/components/divider';
export const metadata: Metadata = {
title: 'Orders',
description: 'Overview of your previous orders.',
};
export default async function Orders() {
const orders = await listOrders();
if (!orders) {
notFound();
}
return (
<div className="w-full" data-testid="orders-page-wrapper">
<div className="mb-8 flex flex-col gap-y-4">
<h1 className="text-2xl-semi">Orders</h1>
<p className="text-base-regular">
View your previous orders and their status. You can also create
returns or exchanges for your orders if needed.
</p>
</div>
<div>
<OrderOverview orders={orders} />
<Divider className="my-16" />
<TransferRequestForm />
</div>
</div>
);
}

View File

@@ -0,0 +1,23 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import { listOrders } from '~/medusa/lib/data/orders';
import Overview from '~/medusa/modules/account/components/overview';
export const metadata: Metadata = {
title: 'Account',
description: 'Overview of your account activity.',
};
export default async function OverviewTemplate() {
const customer = await retrieveCustomer().catch(() => null);
const orders = (await listOrders().catch(() => null)) || null;
if (!customer) {
notFound();
}
return <Overview customer={customer} orders={orders} />;
}

View File

@@ -0,0 +1,54 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import { listRegions } from '~/medusa/lib/data/regions';
import ProfilePhone from '~/medusa/modules/account//components/profile-phone';
import ProfileBillingAddress from '~/medusa/modules/account/components/profile-billing-address';
import ProfileEmail from '~/medusa/modules/account/components/profile-email';
import ProfileName from '~/medusa/modules/account/components/profile-name';
import ProfilePassword from '~/medusa/modules/account/components/profile-password';
export const metadata: Metadata = {
title: 'Profile',
description: 'View and edit your Medusa Store profile.',
};
export default async function Profile() {
const customer = await retrieveCustomer();
const regions = await listRegions();
if (!customer || !regions) {
notFound();
}
return (
<div className="w-full" data-testid="profile-page-wrapper">
<div className="mb-8 flex flex-col gap-y-4">
<h1 className="text-2xl-semi">Profile</h1>
<p className="text-base-regular">
View and update your profile information, including your name, email,
and phone number. You can also update your billing address, or change
your password.
</p>
</div>
<div className="flex w-full flex-col gap-y-8">
<ProfileName customer={customer} />
<Divider />
<ProfileEmail customer={customer} />
<Divider />
<ProfilePhone customer={customer} />
<Divider />
{/* <ProfilePassword customer={customer} />
<Divider /> */}
<ProfileBillingAddress customer={customer} regions={regions} />
</div>
</div>
);
}
const Divider = () => {
return <div className="h-px w-full bg-gray-200" />;
};
``;

View File

@@ -0,0 +1,12 @@
import { Metadata } from 'next';
import LoginTemplate from '~/medusa/modules/account/templates/login-template';
export const metadata: Metadata = {
title: 'Sign in',
description: 'Sign in to your Medusa Store account.',
};
export default function Login() {
return <LoginTemplate />;
}

View File

@@ -0,0 +1,21 @@
import { Toaster } from '@medusajs/ui';
import { retrieveCustomer } from '~/medusa/lib/data';
import { AccountLayout } from '~/medusa/modules/account/templates';
export default async function AccountPageLayout({
dashboard,
login,
}: {
dashboard?: React.ReactNode;
login?: React.ReactNode;
}) {
const customer = await retrieveCustomer().catch(() => null);
return (
<AccountLayout customer={customer}>
{customer ? dashboard : login}
<Toaster />
</AccountLayout>
);
}

View File

@@ -0,0 +1,9 @@
import Spinner from '~/medusa/modules/common/icons/spinner';
export default function Loading() {
return (
<div className="text-ui-fg-base flex h-full w-full items-center justify-center">
<Spinner size={36} />
</div>
);
}

View File

@@ -0,0 +1,5 @@
import SkeletonCartPage from '~/medusa/modules/skeletons/templates/skeleton-cart-page';
export default function Loading() {
return <SkeletonCartPage />;
}

View File

@@ -0,0 +1,21 @@
import { Metadata } from 'next';
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
export const metadata: Metadata = {
title: '404',
description: 'Something went wrong',
};
export default function NotFound() {
return (
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center">
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
<p className="text-small-regular text-ui-fg-base">
The cart you tried to access does not exist. Clear your cookies and try
again.
</p>
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
</div>
);
}

View File

@@ -0,0 +1,23 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveCart } from '~/medusa/lib/data/cart';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import CartTemplate from '~/medusa/modules/cart/templates';
export const metadata: Metadata = {
title: 'Cart',
description: 'View your cart',
};
export default async function Cart() {
const cart = await retrieveCart().catch((error) => {
console.error(error);
return notFound();
});
const customer = await retrieveCustomer();
return <CartTemplate cart={cart} customer={customer} />;
}

View File

@@ -0,0 +1,90 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { StoreRegion } from '@medusajs/types';
import {
getCategoryByHandle,
listCategories,
} from '~/medusa/lib/data/categories';
import { listRegions } from '~/medusa/lib/data/regions';
import CategoryTemplate from '~/medusa/modules/categories/templates';
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
type Props = {
params: Promise<{ category: string[]; countryCode: string }>;
searchParams: Promise<{
sortBy?: SortOptions;
page?: string;
}>;
};
export async function generateStaticParams() {
const product_categories = await listCategories();
if (!product_categories) {
return [];
}
const countryCodes = await listRegions().then((regions: StoreRegion[]) =>
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
);
const categoryHandles = product_categories.map(
(category: any) => category.handle,
);
const staticParams = countryCodes
?.map((countryCode: string | undefined) =>
categoryHandles.map((handle: any) => ({
countryCode,
category: [handle],
})),
)
.flat();
return staticParams;
}
export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params;
try {
const productCategory = await getCategoryByHandle(params.category);
const title = productCategory.name + ' | Medusa Store';
const description = productCategory.description ?? `${title} category.`;
return {
title: `${title} | Medusa Store`,
description,
alternates: {
canonical: `${params.category.join('/')}`,
},
};
} catch (error) {
notFound();
}
}
export default async function CategoryPage(props: Props) {
const searchParams = await props.searchParams;
const params = await props.params;
const { sortBy, page } = searchParams;
const productCategory = await getCategoryByHandle(params.category);
if (!productCategory) {
notFound();
}
return (
<CategoryTemplate
category={productCategory}
sortBy={sortBy}
page={page}
countryCode={params.countryCode}
/>
);
}

View File

@@ -0,0 +1,95 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { StoreCollection, StoreRegion } from '@medusajs/types';
import {
getCollectionByHandle,
listCollections,
} from '~/medusa/lib/data/collections';
import { listRegions } from '~/medusa/lib/data/regions';
import CollectionTemplate from '~/medusa/modules/collections/templates';
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
type Props = {
params: Promise<{ handle: string; countryCode: string }>;
searchParams: Promise<{
page?: string;
sortBy?: SortOptions;
}>;
};
export const PRODUCT_LIMIT = 12;
export async function generateStaticParams() {
const { collections } = await listCollections({
fields: '*products',
});
if (!collections) {
return [];
}
const countryCodes = await listRegions().then(
(regions: StoreRegion[]) =>
regions
?.map((r) => r.countries?.map((c) => c.iso_2))
.flat()
.filter(Boolean) as string[],
);
const collectionHandles = collections.map(
(collection: StoreCollection) => collection.handle,
);
const staticParams = countryCodes
?.map((countryCode: string) =>
collectionHandles.map((handle: string | undefined) => ({
countryCode,
handle,
})),
)
.flat();
return staticParams;
}
export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params;
const collection = await getCollectionByHandle(params.handle);
if (!collection) {
notFound();
}
const metadata = {
title: `${collection.title} | Medusa Store`,
description: `${collection.title} collection`,
} as Metadata;
return metadata;
}
export default async function CollectionPage(props: Props) {
const searchParams = await props.searchParams;
const params = await props.params;
const { sortBy, page } = searchParams;
const collection = await getCollectionByHandle(params.handle).then(
(collection: StoreCollection) => collection,
);
if (!collection) {
notFound();
}
return (
<CollectionTemplate
collection={collection}
page={page}
sortBy={sortBy}
countryCode={params.countryCode}
/>
);
}

View File

@@ -0,0 +1,117 @@
import { use } from 'react';
import { cookies } from 'next/headers';
import { z } from 'zod';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { AppLogo } from '~/components/app-logo';
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadUserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { HomeSidebar } from '@/app/home/(user)/_components/home-sidebar';
import { HomeMenuNavigation } from '@/app/home/(user)/_components/home-menu-navigation';
import { HomeMobileNavigation } from '@/app/home/(user)/_components/home-mobile-navigation';
function UserHomeLayout({ children }: React.PropsWithChildren) {
const state = use(getLayoutState());
if (state.style === 'sidebar') {
return <SidebarLayout>{children}</SidebarLayout>;
}
return <HeaderLayout>{children}</HeaderLayout>;
}
export default withI18n(UserHomeLayout);
function SidebarLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const state = use(getLayoutState());
return (
<UserWorkspaceContextProvider value={workspace}>
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<HomeSidebar />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} />
</PageMobileNavigation>
{children}
</Page>
</SidebarProvider>
</UserWorkspaceContextProvider>
);
}
function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page style={'header'}>
<PageNavigation>
<HomeMenuNavigation workspace={workspace} />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} />
</PageMobileNavigation>
<SidebarProvider defaultOpen>
<Page style={'sidebar'}>
<PageNavigation>
<HomeSidebar />
</PageNavigation>
{children}
</Page>
</SidebarProvider>
</Page>
</UserWorkspaceContextProvider>
);
}
function MobileNavigation({
workspace,
}: {
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>;
}) {
return (
<>
<AppLogo />
<HomeMobileNavigation workspace={workspace} />
</>
);
}
async function getLayoutState() {
const cookieStore = await cookies();
const LayoutStyleSchema = z.enum(['sidebar', 'header', 'custom']);
const layoutStyleCookie = cookieStore.get('layout-style');
const sidebarOpenCookie = cookieStore.get('sidebar:state');
const sidebarOpen = sidebarOpenCookie
? sidebarOpenCookie.value === 'false'
: !personalAccountNavigationConfig.sidebarCollapsed;
const parsedStyle = LayoutStyleSchema.safeParse(layoutStyleCookie?.value);
const style = parsedStyle.success
? parsedStyle.data
: personalAccountNavigationConfig.style;
return {
open: sidebarOpen,
style,
};
}

View File

@@ -0,0 +1,46 @@
import { Metadata } from 'next';
import { StoreCartShippingOption } from '@medusajs/types';
import { listCartOptions, retrieveCart } from '~/medusa/lib/data/cart';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import { getBaseURL } from '~/medusa/lib/util/env';
import CartMismatchBanner from '~/medusa/modules/layout/components/cart-mismatch-banner';
import Footer from '~/medusa/modules/layout/templates/footer';
import Nav from '~/medusa/modules/layout/templates/nav';
import FreeShippingPriceNudge from '~/medusa/modules/shipping/components/free-shipping-price-nudge';
export const metadata: Metadata = {
metadataBase: new URL(getBaseURL()),
};
export default async function PageLayout(props: { children: React.ReactNode }) {
const customer = await retrieveCustomer();
const cart = await retrieveCart();
let shippingOptions: StoreCartShippingOption[] = [];
if (cart) {
const { shipping_options } = await listCartOptions();
shippingOptions = shipping_options;
}
return (
<>
<Nav />
{customer && cart && (
<CartMismatchBanner customer={customer} cart={cart} />
)}
{cart && (
<FreeShippingPriceNudge
variant="popup"
cart={cart}
shippingOptions={shippingOptions}
/>
)}
{props.children}
<Footer />
</>
);
}

View File

@@ -0,0 +1,20 @@
import { Metadata } from 'next';
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
export const metadata: Metadata = {
title: '404',
description: 'Something went wrong',
};
export default function NotFound() {
return (
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
<p className="text-small-regular text-ui-fg-base">
The page you tried to access does not exist.
</p>
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
</div>
);
}

View File

@@ -0,0 +1,5 @@
import SkeletonOrderConfirmed from '~/medusa/modules/skeletons/templates/skeleton-order-confirmed';
export default function Loading() {
return <SkeletonOrderConfirmed />;
}

View File

@@ -0,0 +1,25 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveOrder } from '~/medusa/lib/data/orders';
import OrderCompletedTemplate from '~/medusa/modules/order/templates/order-completed-template';
type Props = {
params: Promise<{ id: string }>;
};
export const metadata: Metadata = {
title: 'Order Confirmed',
description: 'You purchase was successful',
};
export default async function OrderConfirmedPage(props: Props) {
const params = await props.params;
const order = await retrieveOrder(params.id).catch(() => null);
if (!order) {
return notFound();
}
return <OrderCompletedTemplate order={order} />;
}

View File

@@ -0,0 +1,42 @@
import { Heading, Text } from '@medusajs/ui';
import { acceptTransferRequest } from '~/medusa/lib/data/orders';
import TransferImage from '~/medusa/modules/order/components/transfer-image';
export default async function TransferPage({
params,
}: {
params: { id: string; token: string };
}) {
const { id, token } = params;
const { success, error } = await acceptTransferRequest(id, token);
return (
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
<TransferImage />
<div className="flex flex-col gap-y-6">
{success && (
<>
<Heading level="h1" className="text-xl text-zinc-900">
Order transfered!
</Heading>
<Text className="text-zinc-600">
Order {id} has been successfully transfered to the new owner.
</Text>
</>
)}
{!success && (
<>
<Text className="text-zinc-600">
There was an error accepting the transfer. Please try again.
</Text>
{error && (
<Text className="text-red-500">Error message: {error}</Text>
)}
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { Heading, Text } from '@medusajs/ui';
import { declineTransferRequest } from '~/medusa/lib/data/orders';
import TransferImage from '~/medusa/modules/order/components/transfer-image';
export default async function TransferPage({
params,
}: {
params: { id: string; token: string };
}) {
const { id, token } = params;
const { success, error } = await declineTransferRequest(id, token);
return (
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
<TransferImage />
<div className="flex flex-col gap-y-6">
{success && (
<>
<Heading level="h1" className="text-xl text-zinc-900">
Order transfer declined!
</Heading>
<Text className="text-zinc-600">
Transfer of order {id} has been successfully declined.
</Text>
</>
)}
{!success && (
<>
<Text className="text-zinc-600">
There was an error declining the transfer. Please try again.
</Text>
{error && (
<Text className="text-red-500">Error message: {error}</Text>
)}
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import { Heading, Text } from '@medusajs/ui';
import TransferActions from '~/medusa/modules/order/components/transfer-actions';
import TransferImage from '~/medusa/modules/order/components/transfer-image';
export default async function TransferPage({
params,
}: {
params: { id: string; token: string };
}) {
const { id, token } = params;
return (
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
<TransferImage />
<div className="flex flex-col gap-y-6">
<Heading level="h1" className="text-xl text-zinc-900">
Transfer request for order {id}
</Heading>
<Text className="text-zinc-600">
You&#39;ve received a request to transfer ownership of your order (
{id}). If you agree to this request, you can approve the transfer by
clicking the button below.
</Text>
<div className="h-px w-full bg-zinc-200" />
<Text className="text-zinc-600">
If you accept, the new owner will take over all responsibilities and
permissions associated with this order.
</Text>
<Text className="text-zinc-600">
If you do not recognize this request or wish to retain ownership, no
further action is required.
</Text>
<div className="h-px w-full bg-zinc-200" />
<TransferActions id={id} token={token} />
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { Metadata } from 'next';
import { getRegion, listCollections } from '~/medusa/lib/data';
import FeaturedProducts from '~/medusa/modules/home/components/featured-products';
import Hero from '~/medusa/modules/home/components/hero';
export const metadata: Metadata = {
title: 'Medusa Next.js Starter Template',
description:
'A performant frontend ecommerce starter template with Next.js 15 and Medusa.',
};
export default async function Home(props: {
params: Promise<{ countryCode: string }>;
}) {
const params = await props.params;
const { countryCode } = params;
const region = await getRegion(countryCode);
const { collections } = await listCollections({
fields: 'id, handle, title',
});
if (!collections || !region) {
return null;
}
return (
<>
<Hero />
<div className="py-12">
<ul className="flex flex-col gap-x-6">
<FeaturedProducts collections={collections} region={region} />
</ul>
</div>
</>
);
}

View File

@@ -0,0 +1,108 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { listProducts } from '~/medusa/lib/data/products';
import { getRegion, listRegions } from '~/medusa/lib/data/regions';
import ProductTemplate from '~/medusa/modules/products/templates';
type Props = {
params: Promise<{ countryCode: string; handle: string }>;
};
export async function generateStaticParams() {
try {
const countryCodes = await listRegions().then((regions) =>
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
);
if (!countryCodes) {
return [];
}
const promises = countryCodes.map(async (country) => {
const { response } = await listProducts({
countryCode: country,
queryParams: { limit: 100, fields: 'handle' },
});
return {
country,
products: response.products,
};
});
const countryProducts = await Promise.all(promises);
return countryProducts
.flatMap((countryData) =>
countryData.products.map((product) => ({
countryCode: countryData.country,
handle: product.handle,
})),
)
.filter((param) => param.handle);
} catch (error) {
console.error(
`Failed to generate static paths for product pages: ${
error instanceof Error ? error.message : 'Unknown error'
}.`,
);
return [];
}
}
export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params;
const { handle } = params;
const region = await getRegion(params.countryCode);
if (!region) {
notFound();
}
const product = await listProducts({
countryCode: params.countryCode,
queryParams: { handle },
}).then(({ response }) => response.products[0]);
if (!product) {
notFound();
}
return {
title: `${product.title} | Medusa Store`,
description: `${product.title}`,
openGraph: {
title: `${product.title} | Medusa Store`,
description: `${product.title}`,
images: product.thumbnail ? [product.thumbnail] : [],
},
};
}
export default async function ProductPage(props: Props) {
const params = await props.params;
const region = await getRegion(params.countryCode);
if (!region) {
notFound();
}
const pricedProduct = await listProducts({
countryCode: params.countryCode,
queryParams: { handle: params.handle },
}).then(({ response }) => response.products[0]);
if (!pricedProduct) {
notFound();
}
return (
<ProductTemplate
product={pricedProduct}
region={region}
countryCode={params.countryCode}
/>
);
}

View File

@@ -0,0 +1,33 @@
import { Metadata } from 'next';
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
import StoreTemplate from '~/medusa/modules/store/templates';
export const metadata: Metadata = {
title: 'Store',
description: 'Explore all of our products.',
};
type Params = {
searchParams: Promise<{
sortBy?: SortOptions;
page?: string;
}>;
params: Promise<{
countryCode: string;
}>;
};
export default async function StorePage(props: Params) {
const params = await props.params;
const searchParams = await props.searchParams;
const { sortBy, page } = searchParams;
return (
<StoreTemplate
sortBy={sortBy}
page={page}
countryCode={params.countryCode}
/>
);
}

29
app/store/not-found.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { ArrowUpRightMini } from '@medusajs/icons';
import { Text } from '@medusajs/ui';
export const metadata: Metadata = {
title: '404',
description: 'Something went wrong',
};
export default function NotFound() {
return (
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
<p className="text-small-regular text-ui-fg-base">
The page you tried to access does not exist.
</p>
<Link className="group flex items-center gap-x-1" href="/">
<Text className="text-ui-fg-interactive">Go to frontpage</Text>
<ArrowUpRightMini
className="duration-150 ease-in-out group-hover:rotate-45"
color="var(--fg-interactive)"
/>
</Link>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
app/store/twitter-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

View File

@@ -0,0 +1,108 @@
'use client';
import Image from 'next/image';
import { useTranslation } from 'react-i18next';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { PackageHeader } from './package-header';
import { ButtonTooltip } from './ui/button-tooltip';
export interface IAnalysisPackage {
titleKey: string;
price: number;
nrOfAnalyses: number | string;
tagColor: string;
descriptionKey: string;
}
const analysisPackages = [
{
titleKey: 'product:standard.label',
price: 40,
nrOfAnalyses: 4,
tagColor: 'bg-cyan',
descriptionKey: 'marketing:standard.description',
},
{
titleKey: 'product:standardPlus.label',
price: 85,
nrOfAnalyses: 10,
tagColor: 'bg-warning',
descriptionKey: 'product:standardPlus.description',
},
{
titleKey: 'product:premium.label',
price: 140,
nrOfAnalyses: '12+',
tagColor: 'bg-purple',
descriptionKey: 'product:premium.description',
},
] satisfies IAnalysisPackage[];
export default function SelectAnalysisPackages() {
const {
t,
i18n: { language },
} = useTranslation();
return (
<div className="grid grid-cols-3 gap-6">
{analysisPackages.length > 0 ? analysisPackages.map(
(
{ titleKey, price, nrOfAnalyses, tagColor, descriptionKey },
index,
) => {
return (
<Card key={index}>
<CardHeader className="relative">
<ButtonTooltip
content="Content pending"
className="absolute top-5 right-5 z-10"
/>
<Image
src="/assets/card-image.png"
alt="background"
width={326}
height={195}
className="max-h-48 w-full opacity-10"
/>
</CardHeader>
<CardContent className="space-y-1 text-center">
<PackageHeader
title={t(titleKey)}
tagColor={tagColor}
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
language={language}
price={price}
/>
<CardDescription>
<Trans i18nKey={descriptionKey} />
</CardDescription>
</CardContent>
<CardFooter>
<Button className="w-full">
<Trans i18nKey='order-analysis-package:selectThisPackage' />
</Button>
</CardFooter>
</Card>
);
},
) : (
<h4>
<Trans i18nKey='order-analysis-package:noPackagesAvailable' />
</h4>
)}
</div>
);
}

View File

@@ -57,11 +57,11 @@ const pathsConfig = PathsSchema.parse({
accountBillingReturn: `/home/[account]/billing/return`,
joinTeam: '/join',
selectPackage: '/select-package',
booking: '/home/booking',
orderAnalysisPackage: '/home/order-analysis-package',
// these routes are added as placeholders and can be changed when the pages are added
booking: '/booking',
myOrders: '/my-orders',
analysisResults: '/analysis-results',
orderAnalysisPackage: '/order-analysis-package',
orderAnalysis: '/order-analysis',
orderHealthAnalysis: '/order-health-analysis',
},

View File

@@ -137,6 +137,7 @@ async function syncData() {
for (const analysisGroup of analysisGroups) {
// SAVE ANALYSIS GROUP
const { data: insertedAnalysisGroup, error } = await supabase
.schema('medreport')
.from('analysis_groups')
.upsert(
{
@@ -174,6 +175,7 @@ async function syncData() {
const analysisElement = item.UuringuElement;
const { data: insertedAnalysisElement, error } = await supabase
.schema('medreport')
.from('analysis_elements')
.upsert(
{
@@ -217,6 +219,7 @@ async function syncData() {
if (analyses?.length) {
for (const analysis of analyses) {
const { data: insertedAnalysis, error } = await supabase
.schema('medreport')
.from('analyses')
.upsert(
{
@@ -259,7 +262,7 @@ async function syncData() {
}
}
await supabase.from('codes').upsert(codes);
await supabase.schema('medreport').from('codes').upsert(codes);
await supabase.schema('audit').from('sync_entries').insert({
operation: 'ANALYSES_SYNC',

View File

@@ -105,10 +105,12 @@ async function syncData() {
});
const { error: providersError } = await supabase
.schema('medreport')
.from('connected_online_providers')
.upsert(mappedClinics);
const { error: servicesError } = await supabase
.schema('medreport')
.from('connected_online_services')
.upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false });

View File

@@ -34,6 +34,8 @@ export const defaultI18nNamespaces = [
'marketing',
'dashboard',
'product',
'booking',
'order-analysis-package',
];
/**

View File

@@ -1,5 +1,5 @@
'use server'
'use server';
import logRequestResult from '@/lib/services/audit.service';
import { RequestStatus } from '@/lib/types/audit';
import {
@@ -9,7 +9,7 @@ import {
ConnectedOnlineMethodName,
} from '@/lib/types/connected-online';
import { ExternalApi } from '@/lib/types/external';
import { Tables } from '@/supabase/database.types';
import { Tables } from '@/packages/supabase/src/database.types';
import { createClient } from '@/utils/supabase/server';
import axios from 'axios';
@@ -106,11 +106,13 @@ export async function bookAppointment(
{ data: dbService, error: serviceError },
] = await Promise.all([
supabase
.schema('medreport')
.from('connected_online_providers')
.select('*')
.eq('id', clinicId)
.limit(1),
supabase
.schema('medreport')
.from('connected_online_services')
.select('*')
.eq('sync_id', serviceSyncId)
@@ -132,8 +134,14 @@ export async function bookAppointment(
);
}
const clinic: Tables<'connected_online_providers'> = dbClinic![0];
const service: Tables<'connected_online_services'> = dbService![0];
const clinic: Tables<
{ schema: 'medreport' },
'connected_online_providers'
> = dbClinic![0];
const service: Tables<
{ schema: 'medreport' },
'connected_online_services'
> = dbService![0];
// TODO the dummy data needs to be replaced with real values once they're present on the user/account
const response = await axios.post(
@@ -183,6 +191,7 @@ export async function bookAppointment(
const responseParts = responseData.Value.split(',');
const { error } = await supabase
.schema('medreport')
.from('connected_online_reservation')
.insert({
booking_code: responseParts[1],

View File

@@ -32,6 +32,7 @@ import { toArray } from '@/lib/utils';
import axios from 'axios';
import { XMLParser } from 'fast-xml-parser';
import { uniqBy } from 'lodash';
import { Tables } from '@kit/supabase/database';
const BASE_URL = process.env.MEDIPOST_URL!;
@@ -196,6 +197,7 @@ async function saveAnalysisGroup(
supabase: SupabaseClient,
) {
const { data: insertedAnalysisGroup, error } = await supabase
.schema('medreport')
.from('analysis_groups')
.upsert(
{
@@ -215,13 +217,14 @@ async function saveAnalysisGroup(
const analysisGroupId = insertedAnalysisGroup[0].id;
const analysisGroupCodes = toArray(analysisGroup.Kood);
const codes: Partial<Tables<'codes'>>[] = analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: analysisGroupId,
}));
const codes: Partial<Tables<{ schema: 'medreport' }, 'codes'>>[] =
analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
price: kood.Hind,
analysis_group_id: analysisGroupId,
}));
const analysisGroupItems = toArray(analysisGroup.Uuring);
@@ -229,6 +232,7 @@ async function saveAnalysisGroup(
const analysisElement = item.UuringuElement;
const { data: insertedAnalysisElement, error } = await supabase
.schema('medreport')
.from('analysis_elements')
.upsert(
{
@@ -270,6 +274,7 @@ async function saveAnalysisGroup(
if (analyses?.length) {
for (const analysis of analyses) {
const { data: insertedAnalysis, error } = await supabase
.schema('medreport')
.from('analyses')
.upsert(
{
@@ -310,6 +315,7 @@ async function saveAnalysisGroup(
}
const { error: codesError } = await supabase
.schema('medreport')
.from('codes')
.upsert(codes, { ignoreDuplicates: false });
@@ -404,34 +410,41 @@ export async function composeOrderXML(
};
const { data: analysisElements } = (await supabase
.schema('medreport')
.from('analysis_elements')
.select(`*, analysis_groups(*)`)
.in('id', orderedElements)) as {
data: ({
analysis_groups: Tables<'analysis_groups'>;
} & Tables<'analysis_elements'>)[];
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
} & Tables<{ schema: 'medreport' }, 'analysis_elements'>)[];
};
const { data: analyses } = (await supabase
.schema('medreport')
.from('analyses')
.select(`*, analysis_elements(*, analysis_groups(*))`)
.in('id', orderedAnalyses)) as {
data: ({
analysis_elements: Tables<'analysis_elements'> & {
analysis_groups: Tables<'analysis_groups'>;
analysis_elements: Tables<
{ schema: 'medreport' },
'analysis_elements'
> & {
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
};
} & Tables<'analyses'>)[];
} & Tables<{ schema: 'medreport' }, 'analyses'>)[];
};
const analysisGroups: Tables<'analysis_groups'>[] = uniqBy(
(
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ?? []
).concat(
analyses?.flatMap(
({ analysis_elements }) => analysis_elements.analysis_groups,
) ?? [],
),
'id',
);
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
uniqBy(
(
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ??
[]
).concat(
analyses?.flatMap(
({ analysis_elements }) => analysis_elements.analysis_groups,
) ?? [],
),
'id',
);
const specimenSection = [];
const analysisSection = [];
@@ -545,6 +558,7 @@ export async function syncPrivateMessage(
const status = response.TellimuseOlek;
const { data: analysisOrder, error: analysisOrderError } = await supabase
.schema('medreport')
.from('analysis_orders')
.select('user_id')
.eq('id', response.ValisTellimuseId);
@@ -556,6 +570,7 @@ export async function syncPrivateMessage(
}
const { data: analysisResponse, error } = await supabase
.schema('medreport')
.from('analysis_responses')
.upsert(
{
@@ -576,7 +591,7 @@ export async function syncPrivateMessage(
const analysisGroups = toArray(response.UuringuGrupp);
const responses: Omit<
Tables<'analysis_response_elements'>,
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
'id' | 'created_at' | 'updated_at'
>[] = [];
for (const analysisGroup of analysisGroups) {
@@ -608,6 +623,7 @@ export async function syncPrivateMessage(
}
const { error: deleteError } = await supabase
.schema('medreport')
.from('analysis_response_elements')
.delete()
.eq('analysis_response_id', analysisResponse[0].id);
@@ -619,6 +635,7 @@ export async function syncPrivateMessage(
}
const { error: elementInsertError } = await supabase
.schema('medreport')
.from('analysis_response_elements')
.insert(responses);

View File

@@ -1,5 +1,5 @@
import { DATE_TIME_FORMAT } from '@/lib/constants';
import { Tables } from '@/supabase/database.types';
import { Tables } from '@/packages/supabase/src/database.types';
import { format } from 'date-fns';
const isProd = process.env.NODE_ENV === 'production';
@@ -160,7 +160,7 @@ export const getAnalysisGroup = (
analysisGroupOriginalId: string,
analysisGroupName: string,
specimenOrderNr: number,
analysisElement: Tables<'analysis_elements'>,
analysisElement: Tables<{ schema: 'medreport' }, 'analysis_elements'>,
) =>
`<UuringuGrupp>
<UuringuGruppId>${analysisGroupOriginalId}</UuringuGruppId>

View File

@@ -54,6 +54,9 @@
"@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
"@marsidev/react-turnstile": "^1.1.0",
"@medusajs/icons": "^2.8.6",
"@medusajs/js-sdk": "latest",
"@medusajs/ui": "latest",
"@nosecone/next": "1.0.0-beta.7",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-visually-hidden": "^1.2.3",
@@ -85,6 +88,8 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@medusajs/types": "latest",
"@medusajs/ui-preset": "latest",
"@next/bundle-analyzer": "15.3.2",
"@tailwindcss/postcss": "^4.1.10",
"@types/lodash": "^4.17.17",

View File

@@ -20,8 +20,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"zod": "^3.24.4"
"@kit/ui": "workspace:*"
},
"typesVersions": {
"*": {

View File

@@ -1,7 +1,7 @@
import { Database } from '@kit/supabase/database';
export type UpsertSubscriptionParams =
Database['public']['Functions']['upsert_subscription']['Args'] & {
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
line_items: Array<LineItem>;
};
@@ -19,4 +19,4 @@ interface LineItem {
}
export type UpsertOrderParams =
Database['public']['Functions']['upsert_order']['Args'];
Database['medreport']['Functions']['upsert_order']['Args'];

View File

@@ -32,9 +32,7 @@
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "19.1.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"zod": "^3.24.4"
"react-hook-form": "^7.56.3"
},
"typesVersions": {
"*": {

View File

@@ -14,8 +14,8 @@ import { Trans } from '@kit/ui/trans';
import { CurrentPlanBadge } from './current-plan-badge';
import { LineItemDetails } from './line-item-details';
type Order = Tables<'orders'>;
type LineItem = Tables<'order_items'>;
type Order = Tables<{ schema: 'medreport' }, 'orders'>;
type LineItem = Tables<{ schema: 'medreport' }, 'order_items'>;
interface Props {
order: Order & {

View File

@@ -18,8 +18,8 @@ import { CurrentPlanAlert } from './current-plan-alert';
import { CurrentPlanBadge } from './current-plan-badge';
import { LineItemDetails } from './line-item-details';
type Subscription = Tables<'subscriptions'>;
type LineItem = Tables<'subscription_items'>;
type Subscription = Tables<{ schema: 'medreport' }, 'subscriptions'>;
type LineItem = Tables<{ schema: 'medreport' }, 'subscription_items'>;
interface Props {
subscription: Subscription & {

View File

@@ -86,6 +86,7 @@ class BillingEventHandlerService {
logger.info(ctx, 'Processing subscription deleted event...');
const { error } = await client
.schema('medreport')
.from('subscriptions')
.delete()
.match({ id: subscriptionId });
@@ -109,7 +110,7 @@ class BillingEventHandlerService {
logger.info(ctx, 'Successfully deleted subscription');
},
onSubscriptionUpdated: async (subscription) => {
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
const ctx = {
@@ -147,7 +148,7 @@ class BillingEventHandlerService {
onCheckoutSessionCompleted: async (payload) => {
// Handle the checkout session completed event
// here we add the subscription to the database
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
// Check if the payload contains an order_id
@@ -212,7 +213,7 @@ class BillingEventHandlerService {
}
},
onPaymentSucceeded: async (sessionId: string) => {
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
const ctx = {
@@ -244,7 +245,7 @@ class BillingEventHandlerService {
logger.info(ctx, 'Successfully updated payment status');
},
onPaymentFailed: async (sessionId: string) => {
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
const ctx = {

View File

@@ -21,6 +21,7 @@ export async function getBillingGatewayProvider(
async function getBillingProvider(client: SupabaseClient<Database>) {
const { data, error } = await client
.schema('medreport')
.from('config')
.select('billing_provider')
.single();

View File

@@ -4,7 +4,7 @@ import { Tables } from '@kit/supabase/database';
import { createBillingGatewayService } from '../billing-gateway/billing-gateway.service';
type Subscription = Tables<'subscriptions'>;
type Subscription = Tables<{ schema: 'medreport' }, 'subscriptions'>;
export function createBillingWebhooksService() {
return new BillingWebhooksService();

View File

@@ -26,8 +26,7 @@
"@kit/ui": "workspace:*",
"@types/react": "19.1.4",
"next": "15.3.2",
"react": "19.1.0",
"zod": "^3.24.4"
"react": "19.1.0"
},
"typesVersions": {
"*": {

View File

@@ -17,14 +17,14 @@ import { createLemonSqueezySubscriptionPayloadBuilderService } from './lemon-squ
import { createHmac } from './verify-hmac';
type UpsertSubscriptionParams =
Database['public']['Functions']['upsert_subscription']['Args'] & {
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
line_items: Array<LineItem>;
};
type UpsertOrderParams =
Database['public']['Functions']['upsert_order']['Args'];
Database['medreport']['Functions']['upsert_order']['Args'];
type BillingProvider = Enums<'billing_provider'>;
type BillingProvider = Enums<{ schema: 'medreport' }, 'billing_provider'>;
interface LineItem {
id: string;

View File

@@ -30,8 +30,7 @@
"@types/react": "19.1.4",
"date-fns": "^4.1.0",
"next": "15.3.2",
"react": "19.1.0",
"zod": "^3.24.4"
"react": "19.1.0"
},
"typesVersions": {
"*": {

View File

@@ -9,7 +9,7 @@ import { createStripeClient } from './stripe-sdk';
import { createStripeSubscriptionPayloadBuilderService } from './stripe-subscription-payload-builder.service';
type UpsertSubscriptionParams =
Database['public']['Functions']['upsert_subscription']['Args'] & {
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
line_items: Array<LineItem>;
};
@@ -27,9 +27,9 @@ interface LineItem {
}
type UpsertOrderParams =
Database['public']['Functions']['upsert_order']['Args'];
Database['medreport']['Functions']['upsert_order']['Args'];
type BillingProvider = Enums<'billing_provider'>;
type BillingProvider = Enums<{ schema: 'medreport' }, 'billing_provider'>;
export class StripeWebhookHandlerService
implements BillingWebhookHandlerService

View File

@@ -28,8 +28,7 @@
"@kit/ui": "workspace:*",
"@types/node": "^22.15.18",
"@types/react": "19.1.4",
"react": "19.1.0",
"zod": "^3.24.4"
"react": "19.1.0"
},
"typesVersions": {
"*": {

View File

@@ -22,8 +22,7 @@
"@kit/supabase": "workspace:*",
"@kit/team-accounts": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/supabase-js": "2.49.4",
"zod": "^3.24.4"
"@supabase/supabase-js": "2.49.4"
},
"typesVersions": {
"*": {

View File

@@ -1,6 +1,6 @@
import { Database } from '@kit/supabase/database';
export type Tables = Database['public']['Tables'];
export type Tables = Database['medreport']['Tables'];
export type TableChangeType = 'INSERT' | 'UPDATE' | 'DELETE';

View File

@@ -44,9 +44,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3",
"zod": "^3.24.4"
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",
"typesVersions": {

View File

@@ -26,9 +26,10 @@ import { SubMenuModeToggle } from '@kit/ui/mode-toggle';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { usePersonalAccountData } from '../hooks/use-personal-account-data';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
import { toTitleCase } from '~/lib/utils';
const PERSONAL_ACCOUNT_SLUG = 'personal';
@@ -124,7 +125,7 @@ export function PersonalAccountDropdown({
data-test={'account-dropdown-display-name'}
className={'truncate text-sm'}
>
{displayName}
{toTitleCase(displayName)}
</span>
</div>

View File

@@ -19,7 +19,8 @@ import { Trans } from '@kit/ui/trans';
import { useUpdateAccountData } from '../../hooks/use-update-account';
import { AccountDetailsSchema } from '../../schema/account-details.schema';
type UpdateUserDataParams = Database['public']['Tables']['accounts']['Update'];
type UpdateUserDataParams =
Database['medreport']['Tables']['accounts']['Update'];
export function UpdateAccountDetailsForm({
displayName,

View File

@@ -72,6 +72,7 @@ function UploadProfileAvatarForm(props: {
uploadUserProfilePhoto(client, file, props.userId)
.then((pictureUrl) => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: pictureUrl,
@@ -90,6 +91,7 @@ function UploadProfileAvatarForm(props: {
removeExistingStorageFile()
.then(() => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: null,

View File

@@ -17,7 +17,9 @@ interface UserWorkspace {
id: string | null;
name: string | null;
picture_url: string | null;
subscription_status: Tables<'subscriptions'>['status'] | null;
subscription_status:
| Tables<{ schema: 'medreport' }, 'subscriptions'>['status']
| null;
};
user: User;

View File

@@ -21,6 +21,7 @@ export function usePersonalAccountData(
}
const response = await client
.schema('medreport')
.from('accounts')
.select()
.eq('primary_owner_user_id', userId)

View File

@@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query';
import { Database } from '@kit/supabase/database';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
type UpdateData = Database['public']['Tables']['accounts']['Update'];
type UpdateData = Database['medreport']['Tables']['accounts']['Update'];
export function useUpdateAccountData(accountId: string) {
const client = useSupabase();
@@ -11,9 +11,13 @@ export function useUpdateAccountData(accountId: string) {
const mutationKey = ['account:data', accountId];
const mutationFn = async (data: UpdateData) => {
const response = await client.from('accounts').update(data).match({
id: accountId,
});
const response = await client
.schema('medreport')
.from('accounts')
.update(data)
.match({
id: accountId,
});
if (response.error) {
throw response.error;

View File

@@ -17,6 +17,7 @@ class AccountsApi {
*/
async getAccount(id: string) {
const { data, error } = await this.client
.schema('medreport')
.from('accounts')
.select('*')
.eq('id', id)
@@ -35,6 +36,7 @@ class AccountsApi {
*/
async getAccountWorkspace() {
const { data, error } = await this.client
.schema('medreport')
.from('user_account_workspace')
.select(`*`)
.single();
@@ -56,39 +58,43 @@ class AccountsApi {
const { data, error: userError } = authUser;
if (userError) {
console.error('Failed to get user', userError);
throw userError;
}
const { user } = data;
const { data: accounts, error } = await this.client
.schema('medreport')
.from('accounts_memberships')
.select(
`
account_id,
user_accounts (
name,
slug,
picture_url,
)
`,
account_id,
accounts (
name,
slug,
picture_url
)
`,
)
.eq('user_id', user.id)
.eq('account_role', 'owner');
if (error) {
console.error('error', error);
throw error;
}
return accounts.map(({ user_accounts }) => ({
label: user_accounts.name,
value: user_accounts.slug,
image: user_accounts.picture_url,
return accounts.map(({ accounts }) => ({
label: accounts.name,
value: accounts.slug,
image: accounts.picture_url,
}));
}
async loadTempUserAccounts() {
const { data: accounts, error } = await this.client
.schema('medreport')
.from('user_accounts')
.select(`name, slug`);
@@ -111,6 +117,7 @@ class AccountsApi {
*/
async getSubscription(accountId: string) {
const response = await this.client
.schema('medreport')
.from('subscriptions')
.select('*, items: subscription_items !inner (*)')
.eq('account_id', accountId)
@@ -129,6 +136,7 @@ class AccountsApi {
*/
async getOrder(accountId: string) {
const response = await this.client
.schema('medreport')
.from('orders')
.select('*, items: order_items !inner (*)')
.eq('account_id', accountId)
@@ -149,6 +157,7 @@ class AccountsApi {
*/
async getCustomerId(accountId: string) {
const response = await this.client
.schema('medreport')
.from('billing_customers')
.select('customer_id')
.eq('account_id', accountId)

View File

@@ -1,7 +1,10 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"paths": {
"~/lib/utils": ["../../../lib/utils.ts"]
}
},
"include": ["*.ts", "*.tsx", "src"],
"exclude": ["node_modules"]

View File

@@ -28,8 +28,7 @@
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3",
"zod": "^3.24.4"
"react-hook-form": "^7.56.3"
},
"exports": {
".": "./src/index.ts",

View File

@@ -3,6 +3,11 @@ import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react';
import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import {
AccountInvitationsTable,
AccountMembersTable,
InviteMembersDialogContainer,
} from '@kit/team-accounts/components';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Badge } from '@kit/ui/badge';
@@ -28,14 +33,8 @@ import { AdminMembersTable } from './admin-members-table';
import { AdminMembershipsTable } from './admin-memberships-table';
import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog';
import {
AccountInvitationsTable,
AccountMembersTable,
InviteMembersDialogContainer,
} from '@kit/team-accounts/components';
type Account = Tables<'accounts'>;
type Membership = Tables<'accounts_memberships'>;
type Account = Tables<{ schema: 'medreport' }, 'accounts'>;
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>;
export function AdminAccountPage(props: {
account: Account & { memberships: Membership[] };
@@ -231,6 +230,7 @@ async function SubscriptionsTable(props: { accountId: string }) {
const client = getSupabaseServerClient();
const { data: subscription, error } = await client
.schema('medreport')
.from('subscriptions')
.select('*, subscription_items !inner (*)')
.eq('account_id', props.accountId)
@@ -372,6 +372,7 @@ async function getMemberships(userId: string) {
const client = getSupabaseServerClient();
const memberships = await client
.schema('medreport')
.from('accounts_memberships')
.select<
string,
@@ -394,7 +395,7 @@ async function getMemberships(userId: string) {
async function getMembers(accountSlug: string) {
const client = getSupabaseServerClient();
const members = await client.rpc('get_account_members', {
const members = await client.schema('medreport').rpc('get_account_members', {
account_slug: accountSlug,
});

View File

@@ -38,7 +38,7 @@ import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
import { AdminResetPasswordDialog } from './admin-reset-password-dialog';
type Account = Database['public']['Tables']['accounts']['Row'];
type Account = Database['medreport']['Tables']['accounts']['Row'];
const FiltersSchema = z.object({
type: z.enum(['all', 'team', 'personal']),

View File

@@ -9,7 +9,7 @@ import { DataTable } from '@kit/ui/enhanced-data-table';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
type Memberships =
Database['public']['Functions']['get_account_members']['Returns'][number];
Database['medreport']['Functions']['get_account_members']['Returns'][number];
export function AdminMembersTable(props: { members: Memberships[] }) {
return <DataTable data={props.members} columns={getColumns()} />;

View File

@@ -7,7 +7,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { Tables } from '@kit/supabase/database';
import { DataTable } from '@kit/ui/enhanced-data-table';
type Membership = Tables<'accounts_memberships'> & {
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'> & {
account: {
id: string;
name: string;

View File

@@ -15,13 +15,13 @@ import {
ImpersonateUserSchema,
ReactivateUserSchema,
} from './schema/admin-actions.schema';
import { CreateCompanySchema } from './schema/create-company.schema';
import { CreateUserSchema } from './schema/create-user.schema';
import { ResetPasswordSchema } from './schema/reset-password.schema';
import { createAdminAccountsService } from './services/admin-accounts.service';
import { createAdminAuthUserService } from './services/admin-auth-user.service';
import { adminAction } from './utils/admin-action';
import { CreateCompanySchema } from './schema/create-company.schema';
import { createCreateCompanyAccountService } from './services/admin-create-company-account.service';
import { adminAction } from './utils/admin-action';
/**
* @name banUserAction
@@ -183,12 +183,16 @@ export const createUserAction = adminAction(
);
const { error: accountError } = await adminClient
.schema('medreport')
.from('accounts')
.update({ personal_code: personalCode })
.eq('id', data.user.id);
if (accountError) {
logger.error({ accountError }, 'Error inserting personal code to accounts');
logger.error(
{ accountError },
'Error inserting personal code to accounts',
);
throw new Error(`Error saving personal code: ${accountError.message}`);
}

View File

@@ -13,6 +13,7 @@ class AdminAccountsService {
async deleteAccount(accountId: string) {
const { error } = await this.adminClient
.schema('medreport')
.from('accounts')
.delete()
.eq('id', accountId);

View File

@@ -30,6 +30,7 @@ export class AdminDashboardService {
};
const subscriptionsPromise = this.client
.schema('medreport')
.from('subscriptions')
.select('*', selectParams)
.eq('status', 'active')
@@ -47,6 +48,7 @@ export class AdminDashboardService {
});
const trialsPromise = this.client
.schema('medreport')
.from('subscriptions')
.select('*', selectParams)
.eq('status', 'trialing')
@@ -64,6 +66,7 @@ export class AdminDashboardService {
});
const accountsPromise = this.client
.schema('medreport')
.from('accounts')
.select('*', selectParams)
.eq('is_personal_account', true)
@@ -81,6 +84,7 @@ export class AdminDashboardService {
});
const teamAccountsPromise = this.client
.schema('medreport')
.from('accounts')
.select('*', selectParams)
.eq('is_personal_account', false)

View File

@@ -35,9 +35,7 @@
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3",
"zod": "^3.24.4"
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",
"typesVersions": {

Some files were not shown because too many files have changed in this diff Show More