Merge branch 'develop' of https://github.com/MR-medreport/MRB2B into MED-103

This commit is contained in:
Helena
2025-09-08 09:36:46 +03:00
106 changed files with 4793 additions and 4083 deletions

View File

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

15
.env.staging Normal file
View File

@@ -0,0 +1,15 @@
# PRODUCTION ENVIRONMENT VARIABLES
## DO NOT ADD VARS HERE UNLESS THEY ARE PUBLIC OR NOT SENSITIVE
## THIS ENV IS USED FOR PRODUCTION AND IS COMMITED TO THE REPO
## AVOID PLACING SENSITIVE DATA IN THIS FILE.
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
# SUPABASE
# NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
# # MONTONIO
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead

View File

@@ -11,6 +11,7 @@ COPY packages packages
COPY tooling tooling
COPY .env .env
COPY .env.production .env.production
COPY .env.staging .env.staging
# Load env file and echo a specific variable
# RUN dotenv -e .env -- printenv | grep 'SUPABASE' || true
@@ -20,33 +21,33 @@ COPY . .
ENV NODE_ENV=production
# 🔍 Optional: Log key envs for debug
RUN echo "📄 .env.production contents:" && cat .env.production \
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
RUN set -a \
&& . .env \
&& . .env.production \
&& set +a \
&& node check-env.js \
&& pnpm build
&& . .env \
&& . .env.production \
&& . .env.staging \
&& set +a \
&& node check-env.js \
&& pnpm build
# --- Stage 2: Runtime ---
FROM node:20-alpine
ARG APP_ENV=production
WORKDIR /app
COPY --from=builder /app ./
RUN cp ".env.${APP_ENV}" .env.local
RUN npm install -g pnpm@9 \
&& pnpm install --prod --frozen-lockfile
&& pnpm install --prod --frozen-lockfile
ENV NODE_ENV=production
# 🔍 Optional: Log key envs for debug
RUN echo "📄 .env.production contents:" && cat .env.production \
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
RUN echo "📄 .env contents:" && cat .env.local \
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
EXPOSE 3000

View File

@@ -9,18 +9,16 @@ import { ContactEmailSchema } from '../contact-email.schema';
const contactEmail = z
.string({
description: `The email where you want to receive the contact form submissions.`,
required_error:
error:
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
})
}).describe(`The email where you want to receive the contact form submissions.`)
.parse(process.env.CONTACT_EMAIL);
const emailFrom = z
.string({
description: `The email sending address.`,
required_error:
error:
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
})
}).describe(`The email sending address.`)
.parse(process.env.EMAIL_SENDER);
export const sendContactEmail = enhanceAction(

View File

@@ -164,10 +164,15 @@ export default async function syncAnalysisGroups() {
console.info('Inserting sync entry');
await createSyncSuccessEntry();
} catch (e) {
await createSyncFailEntry(JSON.stringify(e));
console.error(e);
const errorMessage = e instanceof Error ? e.message : String(e);
await createSyncFailEntry(JSON.stringify({
message: errorMessage,
stack: e instanceof Error ? e.stack : undefined,
name: e instanceof Error ? e.name : 'Unknown',
}, null, 2));
console.error('Sync failed:', e);
throw new Error(
`Failed to sync public message data, error: ${JSON.stringify(e)}`,
`Failed to sync public message data, error: ${errorMessage}`,
);
}
}

View File

@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
import { retrieveOrder } from "@lib/data/orders";
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
@@ -25,7 +25,7 @@ export const POST = async (request: NextRequest) => {
try {
const medusaOrder = await retrieveOrder(medusaOrderId);
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
console.info("Successfully sent order to medipost");
return NextResponse.json({

View File

@@ -3,7 +3,7 @@ import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
import { getOrderedAnalysisIds } from "~/lib/services/medipost.service";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
const medusaOrder = await retrieveOrder(medusaOrderId)
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
const idsToSend = orderedAnalysisElementsIds;
@@ -35,8 +35,8 @@ export async function POST(request: NextRequest) {
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: [],
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: idsToSend.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at),
});

View File

@@ -3,7 +3,7 @@ import { getOrder } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { createMedipostActionLog, getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
import { createMedipostActionLog, getOrderedAnalysisIds } from "~/lib/services/medipost.service";
export async function POST(request: Request) {
// const isDev = process.env.NODE_ENV === 'development';
@@ -11,16 +11,15 @@ export async function POST(request: Request) {
// return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 });
// }
const { medusaOrderId, maxItems = null } = await request.json();
const { medusaOrderId } = await request.json();
const medusaOrder = await retrieveOrder(medusaOrderId)
const medreportOrder = await getOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} (${maxItems ?? 'all'}) ordered analysis elements`);
const idsToSend = typeof maxItems === 'number' ? orderedAnalysisElementsIds.slice(0, maxItems) : orderedAnalysisElementsIds;
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
const messageXml = await composeOrderTestResponseXML({
person: {
idCode: account.personal_code!,
@@ -28,8 +27,8 @@ export async function POST(request: Request) {
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: [],
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at),
});

View File

@@ -3,17 +3,17 @@ import { z } from 'zod';
export const UpdateAccountSchema = z.object({
firstName: z
.string({
required_error: 'First name is required',
error: 'First name is required',
})
.nonempty(),
lastName: z
.string({
required_error: 'Last name is required',
error: 'Last name is required',
})
.nonempty(),
personalCode: z
.string({
required_error: 'Personal code is required',
error: 'Personal code is required',
})
.nonempty(),
email: z.string().email({
@@ -21,21 +21,25 @@ export const UpdateAccountSchema = z.object({
}),
phone: z
.string({
required_error: 'Phone number is required',
error: 'Phone number is required',
})
.nonempty(),
city: z.string().optional(),
weight: z
.number({
required_error: 'Weight is required',
invalid_type_error: 'Weight must be a number',
error: (issue) =>
issue.input === undefined
? 'Weight is required'
: 'Weight must be a number',
})
.gt(0, { message: 'Weight must be greater than 0' }),
height: z
.number({
required_error: 'Height is required',
invalid_type_error: 'Height must be a number',
error: (issue) =>
issue.input === undefined
? 'Height is required'
: 'Height must be a number',
})
.gt(0, { message: 'Height must be greater than 0' }),
userConsent: z.boolean().refine((val) => val === true, {

View File

@@ -8,7 +8,7 @@ import { listProductTypes } from "@lib/data/products";
import { placeOrder, retrieveCart } from "@lib/data/cart";
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
import { createOrder } from '~/lib/services/order.service';
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
import { createNotificationsApi } from '@kit/notifications/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { AccountWithParams } from '@kit/accounts/api';
@@ -20,12 +20,12 @@ const env = () => z
.object({
emailSender: z
.string({
required_error: 'EMAIL_SENDER is required',
error: 'EMAIL_SENDER is required',
})
.min(1),
siteUrl: z
.string({
required_error: 'NEXT_PUBLIC_SITE_URL is required',
error: 'NEXT_PUBLIC_SITE_URL is required',
})
.min(1),
})
@@ -114,7 +114,7 @@ export async function processMontonioCallback(orderToken: string) {
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
const { productTypes } = await listProductTypes();

View File

@@ -1,17 +1,11 @@
import { use } from 'react';
import { cookies } from 'next/headers';
import { retrieveCart } from '@lib/data/cart';
import { StoreCart } from '@medusajs/types';
import { z } from 'zod';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { AppLogo } from '@kit/shared/components/app-logo';
import {
pathsConfig,
personalAccountNavigationConfig,
} from '@kit/shared/config';
import { pathsConfig } from '@kit/shared/config';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
@@ -24,40 +18,11 @@ import { HomeSidebar } from '../_components/home-sidebar';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
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} cart={null} />
</PageMobileNavigation>
{children}
</Page>
</SidebarProvider>
</UserWorkspaceContextProvider>
);
}
function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const cart = use(retrieveCart());
@@ -101,27 +66,3 @@ function MobileNavigation({
</>
);
}
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

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

View File

@@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next";
import { handleNavigateToPayment } from "@/lib/services/medusaCart.service";
import AnalysisLocation from "./analysis-location";
const IS_DISCOUNT_SHOWN = false as boolean;
const IS_DISCOUNT_SHOWN = true as boolean;
export default function Cart({
cart,
@@ -69,7 +69,7 @@ export default function Cart({
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
const isLocationsShown = synlabAnalyses.length > 0;
return (
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4">
<div className="flex flex-col bg-white gap-y-6">
@@ -77,28 +77,62 @@ export default function Cart({
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
</div>
{hasCartItems && (
<div className="flex justify-end gap-x-4 px-6 py-4">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm">
<Trans i18nKey="cart:total" />
</p>
<>
<div className="flex justify-end gap-x-4 px-6 pt-4">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm text-muted-foreground">
<Trans i18nKey="cart:subtotal" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.subtotal,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
<div className="flex justify-end gap-x-4 px-6 py-2">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm text-muted-foreground">
<Trans i18nKey="cart:promotionsTotal" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.discount_total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div>
</div>
<div className="flex justify-end gap-x-4 px-6">
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm">
<Trans i18nKey="cart:total" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div>
</>
)}
<div className="flex gap-y-6 py-8">
<div className="flex sm:flex-row flex-col gap-y-6 py-8 gap-x-4">
{IS_DISCOUNT_SHOWN && (
<Card
className="flex flex-col justify-between w-1/2"
className="flex flex-col justify-between w-full sm:w-1/2"
>
<CardHeader className="pb-4">
<h5>
@@ -113,7 +147,7 @@ export default function Cart({
{isLocationsShown && (
<Card
className="flex flex-col justify-between w-1/2"
className="flex flex-col justify-between w-full sm:w-1/2"
>
<CardHeader className="pb-4">
<h5>

View File

@@ -0,0 +1,128 @@
'use client';
import Link from 'next/link';
import { BlendingModeIcon } from '@radix-ui/react-icons';
import {
Droplets,
} from 'lucide-react';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardHeader,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
const dummyRecommendations = [
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-cyan/10 text-cyan',
title: 'Kolesterooli kontroll',
description: 'HDL-kolestrool',
tooltipContent: 'Selgitus',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-primary/10 text-primary',
title: 'Kolesterooli kontroll',
tooltipContent: 'Selgitus',
description: 'LDL-Kolesterool',
buttonText: 'Broneeri',
href: '/home/booking',
},
{
icon: <Droplets />,
color: 'bg-destructive/10 text-destructive',
title: 'Vererõhu kontroll',
tooltipContent: 'Selgitus',
description: 'Score-Risk 2',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
];
export default function DashboardRecommendations() {
return (
<Card>
<CardHeader className="items-start">
<h4>
<Trans i18nKey="dashboard:recommendedForYou" />
</h4>
</CardHeader>
<CardContent className="space-y-6">
{dummyRecommendations.map(
(
{
icon,
color,
title,
description,
tooltipContent,
price,
buttonText,
href,
},
index,
) => {
return (
<div
className="flex w-full justify-between gap-3 overflow-scroll"
key={index}
>
<div className="mr-4 flex min-w-fit flex-row items-center gap-4">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
color,
)}
>
{icon}
</div>
<div className="min-w-fit">
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
{title}
<InfoTooltip content={tooltipContent} />
</div>
<p className="text-muted-foreground text-sm">
{description}
</p>
</div>
</div>
<div className="grid w-36 min-w-fit auto-rows-fr grid-cols-2 items-center gap-4">
<p className="text-sm font-medium"> {price}</p>
{href ? (
<Link href={href}>
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
</Link>
) : (
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
)}
</div>
</div>
);
},
)}
</CardContent>
</Card>
);
}

View File

@@ -7,33 +7,41 @@ import { Database } from '@/packages/supabase/src/database.types';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import {
Activity,
ChevronRight,
Clock9,
Droplets,
Pill,
Scale,
TrendingUp,
User,
} from 'lucide-react';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { pathsConfig } from '@kit/shared/config';
import { getPersonParameters } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardProps,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { isNil } from 'lodash';
import { BmiCategory } from '~/lib/types/bmi';
import {
bmiFromMetric,
getBmiBackgroundColor,
getBmiStatus,
} from '~/lib/utils';
import DashboardRecommendations from './dashboard-recommendations';
const getCardVariant = (isSuccess: boolean | null): CardProps['variant'] => {
if (isSuccess === null) return 'default';
if (isSuccess) return 'gradient-success';
return 'gradient-destructive';
};
const cards = ({
gender,
@@ -41,111 +49,91 @@ const cards = ({
height,
weight,
bmiStatus,
smoking,
}: {
gender?: string;
age?: number;
height?: number | null;
weight?: number | null;
bmiStatus: BmiCategory | null;
smoking?: boolean | null;
}) => [
{
title: 'dashboard:gender',
description: gender ?? 'dashboard:male',
icon: <User />,
iconBg: 'bg-success',
},
{
title: 'dashboard:age',
description: age ? `${age}` : '-',
icon: <Clock9 />,
iconBg: 'bg-success',
},
{
title: 'dashboard:height',
description: height ? `${height}cm` : '-',
icon: <RulerHorizontalIcon className="size-4" />,
iconBg: 'bg-success',
},
{
title: 'dashboard:weight',
description: weight ? `${weight}kg` : '-',
icon: <Scale />,
iconBg: 'bg-success',
},
{
title: 'dashboard:bmi',
description: bmiFromMetric(weight || 0, height || 0).toString(),
icon: <TrendingUp />,
iconBg: getBmiBackgroundColor(bmiStatus),
},
{
title: 'dashboard:bloodPressure',
description: '-',
icon: <Activity />,
iconBg: 'bg-warning',
},
{
title: 'dashboard:cholesterol',
description: '-',
icon: <BlendingModeIcon className="size-4" />,
iconBg: 'bg-destructive',
},
{
title: 'dashboard:ldlCholesterol',
description: '-',
icon: <Pill />,
iconBg: 'bg-warning',
},
// {
// title: 'Score 2',
// description: 'Normis',
// icon: <LineChart />,
// iconBg: 'bg-success',
// },
// {
// title: 'dashboard:smoking',
// description: 'dashboard:respondToQuestion',
// descriptionColor: 'text-primary',
// icon: (
// <Button size="icon" variant="outline" className="px-2 text-black">
// <ChevronRight className="size-4 stroke-2" />
// </Button>
// ),
// cardVariant: 'gradient-success' as CardProps['variant'],
// },
];
{
title: 'dashboard:gender',
description: gender ?? 'dashboard:male',
icon: <User />,
iconBg: 'bg-success',
},
{
title: 'dashboard:age',
description: age ? `${age}` : '-',
icon: <Clock9 />,
iconBg: 'bg-success',
},
{
title: 'dashboard:height',
description: height ? `${height}cm` : '-',
icon: <RulerHorizontalIcon className="size-4" />,
iconBg: 'bg-success',
},
{
title: 'dashboard:weight',
description: weight ? `${weight}kg` : '-',
icon: <Scale />,
iconBg: 'bg-success',
},
{
title: 'dashboard:bmi',
description: bmiFromMetric(weight || 0, height || 0).toString(),
icon: <TrendingUp />,
iconBg: getBmiBackgroundColor(bmiStatus),
},
{
title: 'dashboard:bloodPressure',
description: '-',
icon: <Activity />,
iconBg: 'bg-warning',
},
{
title: 'dashboard:cholesterol',
description: '-',
icon: <BlendingModeIcon className="size-4" />,
iconBg: 'bg-destructive',
},
{
title: 'dashboard:ldlCholesterol',
description: '-',
icon: <Pill />,
iconBg: 'bg-warning',
},
// {
// title: 'Score 2',
// description: 'Normis',
// icon: <LineChart />,
// iconBg: 'bg-success',
// },
{
title: 'dashboard:smoking',
description:
isNil(smoking)
? 'dashboard:respondToQuestion'
: !!smoking
? 'common:yes'
: 'common:no',
descriptionColor: 'text-primary',
icon:
isNil(smoking) ? (
<Link href={pathsConfig.app.personalAccountSettings}>
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
) : null,
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
},
];
const dummyRecommendations = [
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-cyan/10 text-cyan',
title: 'Kolesterooli kontroll',
description: 'HDL-kolestrool',
tooltipContent: 'Selgitus',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-primary/10 text-primary',
title: 'Kolesterooli kontroll',
tooltipContent: 'Selgitus',
description: 'LDL-Kolesterool',
buttonText: 'Broneeri',
href: '/home/booking',
},
{
icon: <Droplets />,
color: 'bg-destructive/10 text-destructive',
title: 'Vererõhu kontroll',
tooltipContent: 'Selgitus',
description: 'Score-Risk 2',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
];
const IS_SHOWN_RECOMMENDATIONS = false as boolean;
export default function Dashboard({
account,
@@ -160,8 +148,8 @@ export default function Dashboard({
const params = getPersonParameters(account.personal_code!);
const bmiStatus = getBmiStatus(bmiThresholds, {
age: params?.age || 0,
height: account.account_params?.[0]?.height || 0,
weight: account.account_params?.[0]?.weight || 0,
height: account.accountParams?.height || 0,
weight: account.accountParams?.weight || 0,
});
return (
@@ -170,21 +158,22 @@ export default function Dashboard({
{cards({
gender: params?.gender,
age: params?.age,
height: account.account_params?.[0]?.height,
weight: account.account_params?.[0]?.weight,
height: account.accountParams?.height,
weight: account.accountParams?.weight,
bmiStatus,
smoking: account.accountParams?.isSmoker,
}).map(
({
title,
description,
icon,
iconBg,
// cardVariant,
cardVariant,
// descriptionColor,
}) => (
<Card
key={title}
// variant={cardVariant}
variant={cardVariant}
className="flex flex-col justify-between"
>
<CardHeader className="items-end-safe">
@@ -211,79 +200,7 @@ export default function Dashboard({
),
)}
</div>
<Card>
<CardHeader className="items-start">
<h4>
<Trans i18nKey="dashboard:recommendedForYou" />
</h4>
</CardHeader>
<CardContent className="space-y-6">
{dummyRecommendations.map(
(
{
icon,
color,
title,
description,
tooltipContent,
price,
buttonText,
href,
},
index,
) => {
return (
<div
className="flex w-full justify-between gap-3 overflow-scroll"
key={index}
>
<div className="mr-4 flex min-w-fit flex-row items-center gap-4">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
color,
)}
>
{icon}
</div>
<div className="min-w-fit">
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
{title}
<InfoTooltip content={tooltipContent} />
</div>
<p className="text-muted-foreground text-sm">
{description}
</p>
</div>
</div>
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4 min-w-fit">
<p className="text-sm font-medium"> {price}</p>
{href ? (
<Link href={href}>
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
</Link>
) : (
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
)}
</div>
</div>
);
},
)}
</CardContent>
</Card>
{IS_SHOWN_RECOMMENDATIONS && <DashboardRecommendations />}
</>
);
}

View File

@@ -31,7 +31,8 @@ export async function HomeMenuNavigation(props: {
})
: 0;
const cartQuantityTotal = props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const cartQuantityTotal =
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const hasCartItems = cartQuantityTotal > 0;
return (
@@ -39,15 +40,18 @@ export async function HomeMenuNavigation(props: {
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
<AppLogo href={pathsConfig.app.home} />
</div>
<Search
{/* TODO: add search functionality */}
{/* <Search
className="flex grow"
startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />}
/>
/> */}
<div className="flex items-center justify-end gap-3">
{/* TODO: add wallet functionality
<Card className="px-6 py-2">
<span>€ {Number(0).toFixed(2).replace('.', ',')}</span>
</Card>
*/}
{hasCartItems && (
<Button
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
@@ -56,7 +60,7 @@ export async function HomeMenuNavigation(props: {
<span className="flex items-center text-nowrap">{totalValue}</span>
</Button>
)}
<Link href="/home/cart">
<Link href={pathsConfig.app.cart}>
<Button
variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"

View File

@@ -4,11 +4,13 @@ import { useMemo } from 'react';
import Link from 'next/link';
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
import { StoreCart } from '@medusajs/types';
import { Cross, LogOut, Menu, Shield, ShoppingCart } from 'lucide-react';
import { Cross, Menu, Shield, ShoppingCart } from 'lucide-react';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { ApplicationRoleEnum } from '@kit/accounts/types/accounts';
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
import {
pathsConfig,
personalAccountNavigationConfig,
@@ -91,7 +93,7 @@ export function HomeMobileNavigation(props: {
<If condition={props.cart && hasCartItems}>
<DropdownMenuGroup>
<DropdownLink
path="/home/cart"
path={pathsConfig.app.cart}
label="common:shoppingCartCount"
Icon={<ShoppingCart className="stroke-[1.5px]" />}
labelOptions={{ count: cartQuantityTotal }}
@@ -145,49 +147,4 @@ export function HomeMobileNavigation(props: {
);
}
function DropdownLink(
props: React.PropsWithChildren<{
path: string;
label: string;
labelOptions?: Record<string, any>;
Icon: React.ReactNode;
}>,
) {
return (
<DropdownMenuItem asChild key={props.path}>
<Link
href={props.path}
className={'flex h-12 w-full items-center space-x-4'}
>
{props.Icon}
<span>
<Trans
i18nKey={props.label}
defaults={props.label}
values={props.labelOptions}
/>
</span>
</Link>
</DropdownMenuItem>
);
}
function SignOutDropdownItem(
props: React.PropsWithChildren<{
onSignOut: () => unknown;
}>,
) {
return (
<DropdownMenuItem
className={'flex h-12 w-full items-center space-x-4'}
onClick={props.onSignOut}
>
<LogOut className={'h-6'} />
<span>
<Trans i18nKey={'common:signOut'} defaults={'Sign out'} />
</span>
</DropdownMenuItem>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@kit/ui/button';
import {
@@ -15,12 +16,14 @@ import { handleAddToCart } from '~/lib/services/medusaCart.service';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { Trans } from '@kit/ui/trans';
import { toast } from '@kit/ui/sonner';
import { formatCurrency } from '@/packages/shared/src/utils';
export type OrderAnalysisCard = Pick<
StoreProduct, 'title' | 'description' | 'subtitle'
> & {
isAvailable: boolean;
variant: { id: string };
price: number | null;
};
export default function OrderAnalysesCards({
@@ -30,23 +33,26 @@ export default function OrderAnalysesCards({
analyses: OrderAnalysisCard[];
countryCode: string;
}) {
const [isAddingToCart, setIsAddingToCart] = useState(false);
const { i18n: { language } } = useTranslation()
const [variantAddingToCart, setVariantAddingToCart] = useState<string | null>(null);
const handleSelect = async (variantId: string) => {
if (isAddingToCart) {
if (variantAddingToCart) {
return null;
}
setIsAddingToCart(true);
setVariantAddingToCart(variantId);
try {
await handleAddToCart({
selectedVariant: { id: variantId },
countryCode,
});
toast.success(<Trans i18nKey={'order-analysis:analysisAddedToCart'} />);
setIsAddingToCart(false);
setVariantAddingToCart(null);
} catch (e) {
toast.error(<Trans i18nKey={'order-analysis:analysisAddToCartError'} />);
setIsAddingToCart(false);
setVariantAddingToCart(null);
console.error(e);
}
}
@@ -59,7 +65,15 @@ export default function OrderAnalysesCards({
description,
subtitle,
isAvailable,
price,
}) => {
const formattedPrice = typeof price === 'number'
? formatCurrency({
currencyCode: 'eur',
locale: language,
value: price,
})
: null;
return (
<Card
key={title}
@@ -80,7 +94,7 @@ export default function OrderAnalysesCards({
className="px-2 text-black"
onClick={() => handleSelect(variant.id)}
>
{isAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
{variantAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
</Button>
</div>
)}
@@ -91,7 +105,14 @@ export default function OrderAnalysesCards({
{description && (
<>
{' '}
<InfoTooltip content={`${description}`} />
<InfoTooltip
content={
<div className='flex flex-col gap-2'>
<span>{formattedPrice}</span>
<span>{description}</span>
</div>
}
/>
</>
)}
</h5>

View File

@@ -1,6 +1,5 @@
import { cache } from 'react';
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

View File

@@ -1,11 +1,10 @@
import { cache } from 'react';
import { getProductCategories } from '@lib/data/categories';
import { listProductTypes } from '@lib/data/products';
import { listProducts, listProductTypes } from '@lib/data/products';
import { listRegions } from '@lib/data/regions';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
import { ServiceCategory } from '../../_components/service-categories';
async function countryCodesLoader() {
const countryCodes = await listRegions().then((regions) =>
@@ -39,13 +38,20 @@ async function analysesLoader() {
const category = productCategories.find(
({ metadata }) => metadata?.page === 'order-analysis',
);
const categoryProducts = category
? await listProducts({
countryCode,
queryParams: { limit: 100, category_id: category.id },
})
: null;
const serviceCategories = productCategories.filter(
({ parent_category }) => parent_category?.handle === 'tto-categories',
);
return {
analyses:
category?.products?.map<OrderAnalysisCard>(
categoryProducts?.response.products.map<OrderAnalysisCard>(
({ title, description, subtitle, variants, status, metadata }) => {
const variant = variants![0]!;
return {
@@ -57,6 +63,7 @@ async function analysesLoader() {
},
isAvailable:
status === 'published' && !!metadata?.analysisIdOriginal,
price: variant.calculated_price?.calculated_amount ?? null,
};
},
) ?? [],

View File

@@ -0,0 +1,111 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Trans } from 'react-i18next';
import { AccountWithParams } from '@kit/accounts/api';
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
import { Button } from '@kit/ui/button';
import { Card, CardDescription, CardTitle } from '@kit/ui/card';
import { Form } from '@kit/ui/form';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
import {
AccountPreferences,
accountPreferencesSchema,
} from '../_lib/account-preferences.schema';
import { updatePersonalAccountPreferencesAction } from '../_lib/server/actions';
import { LanguageSelector } from '@kit/ui/language-selector';
export default function AccountPreferencesForm({
account,
}: {
account: AccountWithParams | null;
}) {
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
const form = useForm({
resolver: zodResolver(accountPreferencesSchema),
defaultValues: {
preferredLanguage: account?.preferred_locale,
isConsentToAnonymizedCompanyStatistics:
!!account?.has_consent_anonymized_company_statistics,
},
});
const { register, handleSubmit, watch, setValue } = form;
const onSubmit = async (data: AccountPreferences) => {
if (!account?.id) {
return toast.error(<Trans i18nKey="account:updateAccountError" />);
}
const result = await updatePersonalAccountPreferencesAction({
accountId: account.id,
data,
});
if (result.success) {
revalidateUserDataQuery(account.primary_owner_user_id);
return toast.success(
<Trans i18nKey="account:updateAccountPreferencesSuccess" />,
);
}
return toast.error(
<Trans i18nKey="account:updateAccountPreferencesError" />,
);
};
const watchedConsent = watch('isConsentToAnonymizedCompanyStatistics');
if (!account) return null;
return (
<>
<div className="space-y-2">
<CardTitle className="text-base">
<Trans i18nKey={'account:language'} />
</CardTitle>
<LanguageSelector />
</div>
<Form {...form}>
<form
className="flex flex-col gap-6 text-left"
onSubmit={handleSubmit(onSubmit)}
>
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900">
<Trans i18nKey="account:consents" />
</h2>
<Card>
<div className="flex items-center justify-between p-3">
<div>
<CardTitle className="text-base">
<Trans i18nKey="account:consentToAnonymizedCompanyData.label" />
</CardTitle>
<CardDescription>
<Trans i18nKey="account:consentToAnonymizedCompanyData.description" />
</CardDescription>
</div>
<Switch
checked={!!watchedConsent}
onCheckedChange={(checked) =>
setValue('isConsentToAnonymizedCompanyStatistics', checked)
}
{...register('isConsentToAnonymizedCompanyStatistics')}
/>
</div>
</Card>
</div>
<Button type="submit" className="w-36">
<Trans i18nKey="account:updateProfileSubmitLabel" />
</Button>
</form>
</Form>
</>
);
}

View File

@@ -0,0 +1,259 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Trans } from 'react-i18next';
import { AccountWithParams } from '@kit/accounts/api';
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
import { Button } from '@kit/ui/button';
import { Card, CardTitle } from '@kit/ui/card';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
import {
AccountSettings,
accountSettingsSchema,
} from '../_lib/account-settings.schema';
import { updatePersonalAccountAction } from '../_lib/server/actions';
export default function AccountSettingsForm({
account,
}: {
account: AccountWithParams | null;
}) {
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
const form = useForm({
resolver: zodResolver(accountSettingsSchema),
defaultValues: {
firstName: account?.name,
lastName: account?.last_name ?? '',
email: account?.email,
phone: account?.phone ?? '',
accountParams: {
height: account?.accountParams?.height,
weight: account?.accountParams?.weight,
isSmoker: account?.accountParams?.isSmoker,
},
},
});
const { handleSubmit } = form;
const onSubmit = async (data: AccountSettings) => {
if (!account?.id) {
return toast.error(<Trans i18nKey="account:updateAccountError" />);
}
const result = await updatePersonalAccountAction({
accountId: account.id,
data,
});
if (result.success) {
revalidateUserDataQuery(account.primary_owner_user_id);
return toast.success(<Trans i18nKey="account:updateAccountSuccess" />);
}
return toast.error(<Trans i18nKey="account:updateAccountError" />);
};
if (!account) return null;
return (
<Form {...form}>
<form
className="flex flex-col gap-6 text-left"
onSubmit={handleSubmit(onSubmit)}
>
<FormField
name={'firstName'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField.firstName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
name={'lastName'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField.lastName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
name={'accountParams.height'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField.height'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
name={'accountParams.weight'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField.weight'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<FormField
name={'phone'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField.phone'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
name={'email'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField.email'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900">
<Trans i18nKey="account:myHabits" />
</h2>
<FormField
name="accountParams.isSmoker"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField.smoking'} />
</FormLabel>
<FormControl>
<Select
value={
field.value === true
? 'yes'
: field.value === false
? 'no'
: 'preferNotToAnswer'
}
onValueChange={(value) => {
if (value === 'yes') {
field.onChange(true);
} else if (value === 'no') {
field.onChange(false);
} else {
field.onChange(null);
}
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="yes">
<Trans i18nKey="common:yes" />
</SelectItem>
<SelectItem value="no">
<Trans i18nKey="common:no" />
</SelectItem>
<SelectItem value="preferNotToAnswer">
<Trans i18nKey="common:preferNotToAnswer" />
</SelectItem>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-36">
<Trans i18nKey="account:updateProfileSubmitLabel" />
</Button>
</form>
</Form>
);
}

View File

@@ -0,0 +1,152 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { StoreCart } from '@medusajs/types';
import { Cross, Menu, Shield, ShoppingCart } from 'lucide-react';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { ApplicationRoleEnum } from '@kit/accounts/types/accounts';
import { pathsConfig } from '@kit/shared/config';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
import { UserWorkspace } from '../../_lib/server/load-user-workspace';
import { routes } from './settings-sidebar';
export function SettingsMobileNavigation(props: {
workspace: UserWorkspace;
cart: StoreCart | null;
}) {
const user = props.workspace.user;
const signOut = useSignOut();
const { data: personalAccountData } = usePersonalAccountData(user.id);
const Links = [
{
children: [{ path: pathsConfig.app.home, label: 'common:routes.home' }],
},
]
.concat(routes)
.map((item, index) => {
if ('children' in item) {
return item.children.map((child) => {
return (
<DropdownLink
key={child.path}
path={child.path}
label={child.label}
/>
);
});
}
if ('divider' in item) {
return <DropdownMenuSeparator key={index} />;
}
});
const hasTotpFactor = useMemo(() => {
const factors = user?.factors ?? [];
return factors.some(
(factor) => factor.factor_type === 'totp' && factor.status === 'verified',
);
}, [user?.factors]);
const isSuperAdmin = useMemo(() => {
const hasAdminRole =
personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
return hasAdminRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]);
const isDoctor = useMemo(() => {
const hasDoctorRole =
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
return hasDoctorRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]);
const cartQuantityTotal =
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const hasCartItems = cartQuantityTotal > 0;
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Menu className={'h-9'} />
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
<If condition={props.cart && hasCartItems}>
<DropdownMenuGroup>
<DropdownLink
path={pathsConfig.app.cart}
label="common:shoppingCartCount"
Icon={<ShoppingCart className="stroke-[1.5px]" />}
labelOptions={{ count: cartQuantityTotal }}
/>
</DropdownMenuGroup>
<DropdownMenuSeparator />
</If>
<DropdownMenuGroup>{Links}</DropdownMenuGroup>
<If condition={isSuperAdmin}>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
className={
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
}
href={pathsConfig.app.admin}
>
<Shield className={'h-5'} />
<span>Super Admin</span>
</Link>
</DropdownMenuItem>
</If>
<If condition={isDoctor}>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
className={
'flex h-full cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
}
href={pathsConfig.app.doctor}
>
<Cross className={'h-5'} />
<span>
<Trans i18nKey="common:doctor" />
</span>
</Link>
</DropdownMenuItem>
</If>
<DropdownMenuSeparator />
<SignOutDropdownItem onSignOut={() => signOut.mutateAsync()} />
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,22 @@
import { Separator } from "@kit/ui/separator";
import { Trans } from "@kit/ui/trans";
export default function SettingsSectionHeader({
titleKey,
descriptionKey,
}: {
titleKey: string;
descriptionKey: string;
}) {
return (
<div className="space-y-2">
<h1 className="text-2xl font-semibold text-gray-900">
<Trans i18nKey={titleKey} />
</h1>
<p className="text-gray-600">
<Trans i18nKey={descriptionKey} />
</p>
<Separator />
</div>
);
}

View File

@@ -0,0 +1,55 @@
import z from 'zod';
import { pathsConfig } from '@kit/shared/config';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
import { PageHeader } from '@kit/ui/page';
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarNavigation,
} from '@kit/ui/shadcn-sidebar';
import { Trans } from '@kit/ui/trans';
export const routes = [
{
children: [
{
label: 'common:routes.profile',
path: pathsConfig.app.personalAccountSettings,
end: true,
},
{
label: 'common:routes.preferences',
path: pathsConfig.app.personalAccountPreferences,
end: true,
},
{
label: 'common:routes.security',
path: pathsConfig.app.personalAccountSecurity,
end: true,
},
],
},
] satisfies z.infer<typeof NavigationConfigSchema>['routes'];
export function SettingsSidebar() {
return (
<Sidebar>
<SidebarHeader className="mt-16 h-24 w-[95vw] max-w-screen justify-center border-b bg-white pt-2">
<PageHeader
title={<Trans i18nKey="account:accountTabLabel" />}
description={<Trans i18nKey={'account:accountTabDescription'} />}
/>
</SidebarHeader>
<SidebarContent className="w-auto">
<SidebarNavigation
config={{ style: 'custom', sidebarCollapsedStyle: 'none', routes }}
/>
</SidebarContent>
</Sidebar>
);
}

View File

@@ -0,0 +1,8 @@
import { z } from 'zod';
export const accountPreferencesSchema = z.object({
preferredLanguage: z.enum(['et', 'en', 'ru']).optional().nullable(),
isConsentToAnonymizedCompanyStatistics: z.boolean().optional(),
});
export type AccountPreferences = z.infer<typeof accountPreferencesSchema>;

View File

@@ -0,0 +1,21 @@
import { z } from 'zod';
export const accountSettingsSchema = z.object({
firstName: z
.string()
.min(1, { error: 'error:tooShort' })
.max(200, { error: 'error:tooLong' }),
lastName: z
.string()
.min(1, { error: 'error:tooShort' })
.max(200, { error: 'error:tooLong' }),
email: z.email({ error: 'error:invalidEmail' }).nullable(),
phone: z.e164({ error: 'error:invalidPhone' }),
accountParams: z.object({
height: z.coerce.number({ error: 'error:invalidNumber' }),
weight: z.coerce.number({ error: 'error:invalidNumber' }),
isSmoker: z.boolean().optional().nullable(),
}),
});
export type AccountSettings = z.infer<typeof accountSettingsSchema>;

View File

@@ -0,0 +1,66 @@
'use server';
import z from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { getLogger } from '@kit/shared/logger';
import {
updatePersonalAccount,
updatePersonalAccountPreferences,
} from '~/lib/services/account.service';
import {
AccountPreferences,
accountPreferencesSchema,
} from '../account-preferences.schema';
import {
AccountSettings,
accountSettingsSchema,
} from '../account-settings.schema';
export const updatePersonalAccountAction = enhanceAction(
async ({ accountId, data }: { accountId: string; data: AccountSettings }) => {
const logger = await getLogger();
try {
logger.info({ accountId }, 'Updating account');
await updatePersonalAccount(accountId, data);
logger.info({ accountId }, 'Successfully updated account');
return { success: true };
} catch (e) {
logger.error('Failed to update account', JSON.stringify(e));
return { success: false };
}
},
{
schema: z.object({ accountId: z.uuid(), data: accountSettingsSchema }),
},
);
export const updatePersonalAccountPreferencesAction = enhanceAction(
async ({
accountId,
data,
}: {
accountId: string;
data: AccountPreferences;
}) => {
const logger = await getLogger();
try {
logger.info({ accountId }, 'Updating account preferences');
await updatePersonalAccountPreferences(accountId, data);
logger.info({ accountId }, 'Successfully updated account preferences');
return { success: true };
} catch (e) {
logger.error('Failed to update account preferences', JSON.stringify(e));
return { success: false };
}
},
{
schema: z.object({ accountId: z.uuid(), data: accountPreferencesSchema }),
},
);

View File

@@ -1,23 +1,69 @@
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Trans } from '@kit/ui/trans';
import { use } from 'react';
import { retrieveCart } from '@lib/data/cart';
import { StoreCart } from '@medusajs/types';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { AppLogo } from '@kit/shared/components/app-logo';
import { pathsConfig } from '@kit/shared/config';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SettingsSidebar } from './_components/settings-sidebar';
// home imports
import { HomeMenuNavigation } from '../_components/home-menu-navigation';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
import { SettingsMobileNavigation } from './_components/settings-navigation';
// local imports
import { HomeLayoutPageHeader } from '../_components/home-page-header';
function UserSettingsLayout(props: React.PropsWithChildren) {
return (
<>
<HomeLayoutPageHeader
title={<Trans i18nKey={'account:routes.settings'} />}
description={<AppBreadcrumbs />}
/>
{props.children}
</>
);
function UserSettingsLayout({ children }: React.PropsWithChildren) {
return <HeaderLayout>{children}</HeaderLayout>;
}
export default withI18n(UserSettingsLayout);
function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const cart = use(retrieveCart());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page style={'header'}>
<PageNavigation>
<HomeMenuNavigation workspace={workspace} cart={cart} />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} cart={cart} />
</PageMobileNavigation>
<SidebarProvider defaultOpen>
<Page style={'sidebar'}>
<PageNavigation>
<SettingsSidebar />
</PageNavigation>
<div className="md:mt-28 min-w-full min-h-full">{children}</div>
</Page>
</SidebarProvider>
</Page>
</UserWorkspaceContextProvider>
);
}
function MobileNavigation({
workspace,
cart,
}: {
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>;
cart: StoreCart | null;
}) {
return (
<>
<AppLogo href={pathsConfig.app.home} />
<SettingsMobileNavigation workspace={workspace} cart={cart} />
</>
);
}

View File

@@ -1,28 +1,11 @@
import { use } from 'react';
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
import {
authConfig,
featureFlagsConfig,
pathsConfig,
} from '@kit/shared/config';
import { PageBody } from '@kit/ui/page';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
import { withI18n } from '~/lib/i18n/with-i18n';
const features = {
enableAccountDeletion: featureFlagsConfig.enableAccountDeletion,
enablePasswordUpdate: authConfig.providers.password,
};
const callbackPath = pathsConfig.auth.callback;
const accountHomePath = pathsConfig.app.accountHome;
const paths = {
callback: callbackPath + `?next=${accountHomePath}`,
};
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
import AccountSettingsForm from './_components/account-settings-form';
import SettingsSectionHeader from './_components/settings-section-header';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
@@ -33,17 +16,18 @@ export const generateMetadata = async () => {
};
};
function PersonalAccountSettingsPage() {
const user = use(requireUserInServerComponent());
async function PersonalAccountSettingsPage() {
const account = await loadCurrentUserAccount();
return (
<PageBody>
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
<PersonalAccountSettingsContainer
userId={user.id}
features={features}
paths={paths}
/>
<div className="mx-auto w-full bg-white p-6">
<div className="space-y-6">
<SettingsSectionHeader
titleKey="account:accountTabLabel"
descriptionKey="account:accountTabDescription"
/>
<AccountSettingsForm account={account} />
</div>
</div>
</PageBody>
);

View File

@@ -0,0 +1,24 @@
import { CardTitle } from '@kit/ui/card';
import { LanguageSelector } from '@kit/ui/language-selector';
import { Trans } from '@kit/ui/trans';
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
import AccountPreferencesForm from '../_components/account-preferences-form';
import SettingsSectionHeader from '../_components/settings-section-header';
export default async function PreferencesPage() {
const account = await loadCurrentUserAccount();
return (
<div className="mx-auto w-full bg-white p-6">
<div className="space-y-6">
<SettingsSectionHeader
titleKey="account:preferencesTabLabel"
descriptionKey="account:preferencesTabDescription"
/>
<AccountPreferencesForm account={account} />
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { MultiFactorAuthFactorsList } from '@kit/accounts/components';
import SettingsSectionHeader from '../_components/settings-section-header';
export default function SecuritySettingsPage() {
return (
<div className="mx-auto w-full bg-white p-6">
<div className="space-y-6">
<SettingsSectionHeader
titleKey="account:securityTabLabel"
descriptionKey="account:securityTabDescription"
/>
<MultiFactorAuthFactorsList />
</div>
</div>
);
}

View File

@@ -1,9 +1,10 @@
'use client';
import Link from 'next/link';
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
import { useRouter } from 'next/navigation';
import { Home, LogOut, Menu } from 'lucide-react';
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
import { Home, Menu } from 'lucide-react';
import { AccountSelector } from '@kit/accounts/account-selector';
import {
@@ -92,47 +93,7 @@ export const TeamAccountLayoutMobileNavigation = (
);
};
function DropdownLink(
props: React.PropsWithChildren<{
path: string;
label: string;
Icon: React.ReactNode;
}>,
) {
return (
<DropdownMenuItem asChild>
<Link
href={props.path}
className={'flex h-12 w-full items-center gap-x-3 px-3'}
>
{props.Icon}
<span>
<Trans i18nKey={props.label} defaults={props.label} />
</span>
</Link>
</DropdownMenuItem>
);
}
function SignOutDropdownItem(
props: React.PropsWithChildren<{
onSignOut: () => unknown;
}>,
) {
return (
<DropdownMenuItem
className={'flex h-12 w-full items-center space-x-2'}
onClick={props.onSignOut}
>
<LogOut className={'h-4'} />
<span>
<Trans i18nKey={'common:signOut'} />
</span>
</DropdownMenuItem>
);
}
function TeamAccountsModal(props: {
accounts: Accounts;

View File

@@ -41,6 +41,7 @@ export const defaultI18nNamespaces = [
'orders',
'analysis-results',
'doctor',
'error',
];
/**

View File

@@ -1,8 +1,14 @@
import type { Tables } from '@/packages/supabase/src/database.types';
import { AccountWithParams } from '@kit/accounts/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AccountSettings } from '~/home/(user)/settings/_lib/account-settings.schema';
import { AccountPreferences } from '../../app/home/(user)/settings/_lib/account-preferences.schema';
import { updateCustomer } from '../../packages/features/medusa-storefront/src/lib/data';
type Account = Tables<{ schema: 'medreport' }, 'accounts'>;
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>;
@@ -61,7 +67,9 @@ export async function getDoctorAccounts() {
}
export async function getAssignedDoctorAccount(analysisOrderId: number) {
const { data: doctorUser } = await getSupabaseServerAdminClient()
const supabase = getSupabaseServerAdminClient();
const { data: doctorUser } = await supabase
.schema('medreport')
.from('doctor_analysis_feedback')
.select('doctor_user_id')
@@ -73,7 +81,7 @@ export async function getAssignedDoctorAccount(analysisOrderId: number) {
return null;
}
const { data } = await getSupabaseServerAdminClient()
const { data } = await supabase
.schema('medreport')
.from('accounts')
.select('email')
@@ -81,3 +89,58 @@ export async function getAssignedDoctorAccount(analysisOrderId: number) {
return { email: data?.[0]?.email };
}
export async function updatePersonalAccount(
accountId: string,
account: AccountSettings,
) {
const supabase = getSupabaseServerClient();
return Promise.all([
supabase
.schema('medreport')
.from('accounts')
.update({
name: account.firstName,
last_name: account.lastName,
email: account.email,
phone: account.phone,
})
.eq('id', accountId)
.throwOnError(),
supabase
.schema('medreport')
.from('account_params')
.upsert(
{
height: account.accountParams.height,
weight: account.accountParams.weight,
is_smoker: account.accountParams.isSmoker,
},
{ onConflict: 'account_id' },
)
.throwOnError(),
updateCustomer({
first_name: account.firstName,
last_name: account.lastName,
phone: account.phone,
}),
]);
}
export async function updatePersonalAccountPreferences(
accountId: string,
preferences: AccountPreferences,
) {
const supabase = getSupabaseServerClient();
return supabase
.schema('medreport')
.from('accounts')
.update({
preferred_locale: preferences.preferredLanguage,
has_consent_anonymized_company_statistics:
preferences.isConsentToAnonymizedCompanyStatistics,
})
.eq('id', accountId)
.throwOnError();
}

View File

@@ -105,12 +105,18 @@ export const createMedusaSyncSuccessEntry = async () => {
});
}
export async function getAnalyses({ ids }: { ids: number[] }): Promise<AnalysesWithGroupsAndElements> {
const { data } = await getSupabaseServerAdminClient()
export async function getAnalyses({ ids, originalIds }: { ids?: number[], originalIds?: string[] }): Promise<AnalysesWithGroupsAndElements> {
const query = getSupabaseServerAdminClient()
.schema('medreport')
.from('analyses')
.select(`*, analysis_elements(*, analysis_groups(*))`)
.in('id', ids);
.select(`*, analysis_elements(*, analysis_groups(*))`);
if (Array.isArray(ids)) {
query.in('id', ids);
}
if (Array.isArray(originalIds)) {
query.in('analysis_id_original', originalIds);
}
const { data } = await query.throwOnError();
return data as unknown as AnalysesWithGroupsAndElements;
}

View File

@@ -10,6 +10,7 @@ import {
getClientInstitution,
getClientPerson,
getConfidentiality,
getOrderEnteredPerson,
getPais,
getPatient,
getProviderInstitution,
@@ -97,7 +98,7 @@ export async function getLatestPublicMessageListItem() {
Action: MedipostAction.GetPublicMessageList,
User: USER,
Password: PASSWORD,
Sender: 'syndev',
Sender: RECIPIENT,
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
// MessageType check only for messages of certain type
},
@@ -553,14 +554,12 @@ export async function composeOrderXML({
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
<Tellimus cito="EI">
<ValisTellimuseId>${orderId}</ValisTellimuseId>
<!--<TellijaAsutus>-->
${getClientInstitution()}
<!--<TeostajaAsutus>-->
${getProviderInstitution()}
<!--<TellijaIsik>-->
${getClientPerson(person)}
${getClientPerson()}
${getOrderEnteredPerson()}
<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
${getPatient(person)}
${getPatient(person)}
${getConfidentiality()}
${specimenSection.join('')}
${analysisSection?.join('')}
@@ -666,7 +665,7 @@ async function syncPrivateMessage({
unit: element.Mootyhik ?? null,
original_response_element: element,
analysis_name: element.UuringNimi || element.KNimetus,
comment: element.UuringuKommentaar
comment: element.UuringuKommentaar ?? '',
})),
);
}
@@ -715,7 +714,7 @@ export async function sendOrderToMedipost({
orderedAnalysisElements,
}: {
medusaOrderId: string;
orderedAnalysisElements: { analysisElementId: number }[];
orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[];
}) {
const medreportOrder = await getOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
@@ -727,8 +726,8 @@ export async function sendOrderToMedipost({
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: [],
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at),
comment: '',
@@ -784,12 +783,13 @@ export async function sendOrderToMedipost({
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
}
export async function getOrderedAnalysisElementsIds({
export async function getOrderedAnalysisIds({
medusaOrder,
}: {
medusaOrder: StoreOrder;
}): Promise<{
analysisElementId: number;
analysisElementId?: number;
analysisId?: number;
}[]> {
const countryCodes = await listRegions();
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
@@ -802,6 +802,14 @@ export async function getOrderedAnalysisElementsIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
}
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
const originalIds = (medusaOrder?.items ?? [])
.map((a) => a.product?.metadata?.analysisIdOriginal)
.filter((a) => typeof a === 'string') as string[];
const analyses = await getAnalyses({ originalIds });
return analyses.map(({ id }) => ({ analysisId: id }));
}
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
@@ -841,12 +849,13 @@ export async function getOrderedAnalysisElementsIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
}
const [analysisPackageElements, orderedAnalysisElements] = await Promise.all([
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
getOrderedAnalysisPackages(medusaOrder),
getOrderedAnalysisElements(medusaOrder),
getOrderedAnalyses(medusaOrder),
]);
return [...analysisPackageElements, ...orderedAnalysisElements];
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
}
export async function createMedipostActionLog({

View File

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

View File

@@ -16,12 +16,12 @@ const env = () =>
.object({
medusaBackendPublicUrl: z
.string({
required_error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
})
.min(1),
siteUrl: z
.string({
required_error: 'NEXT_PUBLIC_SITE_URL is required',
error: 'NEXT_PUBLIC_SITE_URL is required',
})
.min(1),
})

View File

@@ -10,7 +10,7 @@ export async function createOrder({
orderedAnalysisElements,
}: {
medusaOrder: StoreOrder;
orderedAnalysisElements: { analysisElementId: number }[];
orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[];
}) {
const supabase = getSupabaseServerClient();
@@ -21,8 +21,8 @@ export async function createOrder({
const orderResult = await supabase.schema('medreport')
.from('analysis_orders')
.insert({
analysis_element_ids: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
analysis_ids: [],
analysis_element_ids: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
analysis_ids: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
status: 'QUEUED',
user_id: user.id,
medusa_order_id: medusaOrder.id,

View File

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

View File

@@ -2,15 +2,14 @@ import { z } from 'zod';
export const companyOfferSchema = z.object({
companyName: z.string({
required_error: 'Company name is required',
error: 'Company name is required',
}),
contactPerson: z.string({
required_error: 'Contact person is required',
error: 'Contact person is required',
}),
email: z
.string({
required_error: 'Email is required',
})
.email('Invalid email'),
.email({
error: 'Invalid email',
}),
phone: z.string().optional(),
});

View File

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

View File

@@ -33,13 +33,13 @@
"@hookform/resolvers": "^5.1.1",
"@kit/accounts": "workspace:*",
"@kit/admin": "workspace:*",
"@kit/doctor": "workspace:*",
"@kit/analytics": "workspace:*",
"@kit/auth": "workspace:*",
"@kit/billing": "workspace:*",
"@kit/billing-gateway": "workspace:*",
"@kit/cms": "workspace:*",
"@kit/database-webhooks": "workspace:*",
"@kit/doctor": "workspace:*",
"@kit/email-templates": "workspace:*",
"@kit/i18n": "workspace:*",
"@kit/mailers": "workspace:*",
@@ -82,7 +82,7 @@
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.1",
"ts-node": "^10.9.2",
"zod": "^3.25.67"
"zod": "^4.1.5"
},
"devDependencies": {
"@hookform/resolvers": "^5.0.1",

View File

@@ -21,39 +21,25 @@ export const PaymentTypeSchema = z.enum(['one-time', 'recurring']);
export const LineItemSchema = z
.object({
id: z
.string({
description:
'Unique identifier for the line item. Defined by the Provider.',
})
.string()
.describe('Unique identifier for the line item. Defined by the Provider.')
.min(1),
name: z
.string({
description: 'Name of the line item. Displayed to the user.',
})
.string().describe('Name of the line item. Displayed to the user.')
.min(1),
description: z
.string({
description:
'Description of the line item. Displayed to the user and will replace the auto-generated description inferred' +
' from the line item. This is useful if you want to provide a more detailed description to the user.',
})
.string().describe('Description of the line item. Displayed to the user and will replace the auto-generated description inferred' +
' from the line item. This is useful if you want to provide a more detailed description to the user.')
.optional(),
cost: z
.number({
description: 'Cost of the line item. Displayed to the user.',
})
.number().describe('Cost of the line item. Displayed to the user.')
.min(0),
type: LineItemTypeSchema,
unit: z
.string({
description:
'Unit of the line item. Displayed to the user. Example "seat" or "GB"',
})
.string().describe('Unit of the line item. Displayed to the user. Example "seat" or "GB"')
.optional(),
setupFee: z
.number({
description: `Lemon Squeezy only: If true, in addition to the cost, a setup fee will be charged.`,
})
.number().describe(`Lemon Squeezy only: If true, in addition to the cost, a setup fee will be charged.`)
.positive()
.optional(),
tiers: z
@@ -92,14 +78,10 @@ export const LineItemSchema = z
export const PlanSchema = z
.object({
id: z
.string({
description: 'Unique identifier for the plan. Defined by yourself.',
})
.string().describe('Unique identifier for the plan. Defined by yourself.')
.min(1),
name: z
.string({
description: 'Name of the plan. Displayed to the user.',
})
.string().describe('Name of the plan. Displayed to the user.')
.min(1),
interval: BillingIntervalSchema.optional(),
custom: z.boolean().default(false).optional(),
@@ -124,10 +106,7 @@ export const PlanSchema = z
},
),
trialDays: z
.number({
description:
'Number of days for the trial period. Leave empty for no trial.',
})
.number().describe('Number of days for the trial period. Leave empty for no trial.')
.positive()
.optional(),
paymentType: PaymentTypeSchema,
@@ -209,54 +188,34 @@ export const PlanSchema = z
const ProductSchema = z
.object({
id: z
.string({
description:
'Unique identifier for the product. Defined by th Provider.',
})
.string().describe('Unique identifier for the product. Defined by th Provider.')
.min(1),
name: z
.string({
description: 'Name of the product. Displayed to the user.',
})
.string().describe('Name of the product. Displayed to the user.')
.min(1),
description: z
.string({
description: 'Description of the product. Displayed to the user.',
})
.string().describe('Description of the product. Displayed to the user.')
.min(1),
currency: z
.string({
description: 'Currency code for the product. Displayed to the user.',
})
.string().describe('Currency code for the product. Displayed to the user.')
.min(3)
.max(3),
badge: z
.string({
description:
'Badge for the product. Displayed to the user. Example: "Popular"',
})
.string().describe('Badge for the product. Displayed to the user. Example: "Popular"')
.optional(),
features: z
.array(
z.string({
description: 'Features of the product. Displayed to the user.',
}),
)
z.string(),
).describe('Features of the product. Displayed to the user.')
.nonempty(),
enableDiscountField: z
.boolean({
description: 'Enable discount field for the product in the checkout.',
})
.boolean().describe('Enable discount field for the product in the checkout.')
.optional(),
highlighted: z
.boolean({
description: 'Highlight this product. Displayed to the user.',
})
.boolean().describe('Highlight this product. Displayed to the user.')
.optional(),
hidden: z
.boolean({
description: 'Hide this product from being displayed to users.',
})
.boolean().describe('Hide this product from being displayed to users.')
.optional(),
plans: z.array(PlanSchema),
})

View File

@@ -1,14 +1,10 @@
import { z } from 'zod';
export const ReportBillingUsageSchema = z.object({
id: z.string({
description:
'The id of the usage record. For Stripe a customer ID, for LS a subscription item ID.',
}),
id: z.string().describe('The id of the usage record. For Stripe a customer ID, for LS a subscription item ID.'),
eventName: z
.string({
description: 'The name of the event that triggered the usage',
})
.string()
.describe('The name of the event that triggered the usage')
.optional(),
usage: z.object({
quantity: z.number(),

View File

@@ -3,8 +3,8 @@ import { z } from 'zod';
export const UpdateHealthBenefitSchema = z.object({
occurance: z
.string({
required_error: 'Occurance is required',
error: 'Occurance is required',
})
.nonempty(),
amount: z.number({ required_error: 'Amount is required' }),
amount: z.number({ error: 'Amount is required' }),
});

View File

@@ -4,12 +4,12 @@ export const MontonioServerEnvSchema = z
.object({
secretKey: z
.string({
required_error: `Please provide the variable MONTONIO_SECRET_KEY`,
error: `Please provide the variable MONTONIO_SECRET_KEY`,
})
.min(1),
apiUrl: z
.string({
required_error: `Please provide the variable MONTONIO_API_URL`,
error: `Please provide the variable MONTONIO_API_URL`,
})
.min(1),
});

View File

@@ -4,12 +4,12 @@ export const StripeServerEnvSchema = z
.object({
secretKey: z
.string({
required_error: `Please provide the variable STRIPE_SECRET_KEY`,
error: `Please provide the variable STRIPE_SECRET_KEY`,
})
.min(1),
webhooksSecret: z
.string({
required_error: `Please provide the variable STRIPE_WEBHOOK_SECRET`,
error: `Please provide the variable STRIPE_WEBHOOK_SECRET`,
})
.min(1),
})

View File

@@ -4,9 +4,9 @@ import { DatabaseWebhookVerifierService } from './database-webhook-verifier.serv
const webhooksSecret = z
.string({
description: `The secret used to verify the webhook signature`,
required_error: `Provide the variable SUPABASE_DB_WEBHOOK_SECRET. This is used to authenticate the webhook event from Supabase.`,
error: `Provide the variable SUPABASE_DB_WEBHOOK_SECRET. This is used to authenticate the webhook event from Supabase.`,
})
.describe(`The secret used to verify the webhook signature`,)
.min(1)
.parse(process.env.SUPABASE_DB_WEBHOOK_SECRET);

View File

@@ -1 +1,3 @@
export * from './user-workspace-context';
export * from './personal-account-settings/mfa/multi-factor-auth-list'
export * from './personal-account-settings/mfa/multi-factor-auth-setup-dialog'

View File

@@ -101,14 +101,14 @@ export function PersonalAccountDropdown({
personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
return hasAdminRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]);
}, [personalAccountData, hasTotpFactor]);
const isDoctor = useMemo(() => {
const hasDoctorRole =
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
return hasDoctorRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]);
}, [personalAccountData, hasTotpFactor]);
return (
<DropdownMenu>

View File

@@ -1,208 +0,0 @@
'use client';
import { useTranslation } from 'react-i18next';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { If } from '@kit/ui/if';
import { LanguageSelector } from '@kit/ui/language-selector';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
import { Trans } from '@kit/ui/trans';
import { usePersonalAccountData } from '../../hooks/use-personal-account-data';
import { AccountDangerZone } from './account-danger-zone';
import ConsentToggle from './consent/consent-toggle';
import { UpdateEmailFormContainer } from './email/update-email-form-container';
import { MultiFactorAuthFactorsList } from './mfa/multi-factor-auth-list';
import { UpdatePasswordFormContainer } from './password/update-password-container';
import { UpdateAccountDetailsFormContainer } from './update-account-details-form-container';
import { UpdateAccountImageContainer } from './update-account-image-container';
export function PersonalAccountSettingsContainer(
props: React.PropsWithChildren<{
userId: string;
features: {
enableAccountDeletion: boolean;
enablePasswordUpdate: boolean;
};
paths: {
callback: string;
};
}>,
) {
const supportsLanguageSelection = useSupportMultiLanguage();
const user = usePersonalAccountData(props.userId);
if (!user.data || user.isPending) {
return <LoadingOverlay fullPage />;
}
return (
<div className={'flex w-full flex-col space-y-4 pb-32'}>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:accountImage'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:accountImageDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<UpdateAccountImageContainer
user={{
pictureUrl: user.data.picture_url,
id: user.data.id,
}}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:name'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:nameDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<UpdateAccountDetailsFormContainer user={user.data} />
</CardContent>
</Card>
<If condition={supportsLanguageSelection}>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:language'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:languageDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<LanguageSelector />
</CardContent>
</Card>
</If>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:updateEmailCardTitle'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:updateEmailCardDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<UpdateEmailFormContainer callbackPath={props.paths.callback} />
</CardContent>
</Card>
<If condition={props.features.enablePasswordUpdate}>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:updatePasswordCardTitle'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:updatePasswordCardDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<UpdatePasswordFormContainer callbackPath={props.paths.callback} />
</CardContent>
</Card>
</If>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:multiFactorAuth'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:multiFactorAuthDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<MultiFactorAuthFactorsList userId={props.userId} />
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex justify-between">
<div>
<p className="font-medium">
<Trans
i18nKey={'account:consentToAnonymizedCompanyData.label'}
/>
</p>
<CardDescription>
<Trans
i18nKey={'account:consentToAnonymizedCompanyData.description'}
/>
</CardDescription>
</div>
<ConsentToggle
userId={props.userId}
initialState={
!!user.data.has_consent_anonymized_company_statistics
}
/>
</div>
</CardHeader>
</Card>
<If condition={props.features.enableAccountDeletion}>
<Card className={'border-destructive'}>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:dangerZone'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:dangerZoneDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<AccountDangerZone />
</CardContent>
</Card>
</If>
</div>
);
}
function useSupportMultiLanguage() {
const { i18n } = useTranslation();
const langs = (i18n?.options?.supportedLngs as string[]) ?? [];
const supportedLangs = langs.filter((lang) => lang !== 'cimode');
return supportedLangs.length > 1;
}

View File

@@ -1,46 +0,0 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { Switch } from '@kit/ui/switch';
import { Trans } from '@kit/ui/trans';
import { useRevalidatePersonalAccountDataQuery } from '../../../hooks/use-personal-account-data';
import { useUpdateAccountData } from '../../../hooks/use-update-account';
// This is temporary. When the profile views are ready, all account values included in the form will be updated together on form submit.
export default function ConsentToggle({
userId,
initialState,
}: {
userId: string;
initialState: boolean;
}) {
const [isConsent, setIsConsent] = useState(initialState);
const updateAccountMutation = useUpdateAccountData(userId);
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
const updateConsent = (consent: boolean) => {
const promise = updateAccountMutation
.mutateAsync({
has_consent_anonymized_company_statistics: consent,
})
.then(() => {
revalidateUserDataQuery(userId);
});
return toast.promise(() => promise, {
success: <Trans i18nKey={'account:updateConsentSuccess'} />,
error: <Trans i18nKey={'account:updateConsentError'} />,
loading: <Trans i18nKey={'account:updateConsentLoading'} />,
});
};
return (
<Switch
checked={isConsent}
onCheckedChange={setIsConsent}
onClick={() => updateConsent(!isConsent)}
disabled={updateAccountMutation.isPending}
/>
);
}

View File

@@ -12,6 +12,7 @@ import { toast } from 'sonner';
import { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useUser } from '@kit/supabase/hooks/use-user';
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import {
@@ -46,13 +47,18 @@ import { Trans } from '@kit/ui/trans';
import { MultiFactorAuthSetupDialog } from './multi-factor-auth-setup-dialog';
export function MultiFactorAuthFactorsList(props: { userId: string }) {
export function MultiFactorAuthFactorsList() {
const { data: user } = useUser();
if (!user?.id) {
return null;
}
return (
<div className={'flex flex-col space-y-4'}>
<FactorsTableContainer userId={props.userId} />
<FactorsTableContainer userId={user?.id} />
<div>
<MultiFactorAuthSetupDialog userId={props.userId} />
<MultiFactorAuthSetupDialog userId={user?.id} />
</div>
</div>
);

View File

@@ -1,42 +0,0 @@
'use client';
import { useUser } from '@kit/supabase/hooks/use-user';
import { Alert } from '@kit/ui/alert';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
import { Trans } from '@kit/ui/trans';
import { UpdatePasswordForm } from './update-password-form';
export function UpdatePasswordFormContainer(
props: React.PropsWithChildren<{
callbackPath: string;
}>,
) {
const { data: user, isPending } = useUser();
if (isPending) {
return <LoadingOverlay fullPage={false} />;
}
if (!user) {
return null;
}
const canUpdatePassword = user.identities?.some(
(item) => item.provider === `email`,
);
if (!canUpdatePassword) {
return <WarnCannotUpdatePasswordAlert />;
}
return <UpdatePasswordForm callbackPath={props.callbackPath} user={user} />;
}
function WarnCannotUpdatePasswordAlert() {
return (
<Alert variant={'warning'}>
<Trans i18nKey={'account:cannotUpdatePassword'} />
</Alert>
);
}

View File

@@ -1,206 +0,0 @@
'use client';
import { useState } from 'react';
import type { User } from '@supabase/supabase-js';
import { zodResolver } from '@hookform/resolvers/zod';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { Check } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
import { PasswordUpdateSchema } from '../../../schema/update-password.schema';
export const UpdatePasswordForm = ({
user,
callbackPath,
}: {
user: User;
callbackPath: string;
}) => {
const { t } = useTranslation('account');
const updateUserMutation = useUpdateUser();
const [needsReauthentication, setNeedsReauthentication] = useState(false);
const updatePasswordFromCredential = (password: string) => {
const redirectTo = [window.location.origin, callbackPath].join('');
const promise = updateUserMutation
.mutateAsync({ password, redirectTo })
.catch((error) => {
if (
typeof error === 'string' &&
error?.includes('Password update requires reauthentication')
) {
setNeedsReauthentication(true);
} else {
throw error;
}
});
toast.promise(() => promise, {
success: t(`updatePasswordSuccess`),
error: t(`updatePasswordError`),
loading: t(`updatePasswordLoading`),
});
};
const updatePasswordCallback = async ({
newPassword,
}: {
newPassword: string;
}) => {
const email = user.email;
// if the user does not have an email assigned, it's possible they
// don't have an email/password factor linked, and the UI is out of sync
if (!email) {
return Promise.reject(t(`cannotUpdatePassword`));
}
updatePasswordFromCredential(newPassword);
};
const form = useForm({
resolver: zodResolver(
PasswordUpdateSchema.withTranslation(t('passwordNotMatching')),
),
defaultValues: {
newPassword: '',
repeatPassword: '',
},
});
return (
<Form {...form}>
<form
data-test={'account-password-form'}
onSubmit={form.handleSubmit(updatePasswordCallback)}
>
<div className={'flex flex-col space-y-4'}>
<If condition={updateUserMutation.data}>
<SuccessAlert />
</If>
<If condition={needsReauthentication}>
<NeedsReauthenticationAlert />
</If>
<FormField
name={'newPassword'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Label>
<Trans i18nKey={'account:newPassword'} />
</Label>
</FormLabel>
<FormControl>
<Input
data-test={'account-password-form-password-input'}
autoComplete={'new-password'}
required
type={'password'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
name={'repeatPassword'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Label>
<Trans i18nKey={'account:repeatPassword'} />
</Label>
</FormLabel>
<FormControl>
<Input
data-test={'account-password-form-repeat-password-input'}
required
type={'password'}
{...field}
/>
</FormControl>
<FormDescription>
<Trans i18nKey={'account:repeatPasswordDescription'} />
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<div>
<Button disabled={updateUserMutation.isPending}>
<Trans i18nKey={'account:updatePasswordSubmitLabel'} />
</Button>
</div>
</div>
</form>
</Form>
);
};
function SuccessAlert() {
return (
<Alert variant={'success'}>
<Check className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'account:updatePasswordSuccess'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
</AlertDescription>
</Alert>
);
}
function NeedsReauthenticationAlert() {
return (
<Alert variant={'warning'}>
<ExclamationTriangleIcon className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'account:needsReauthentication'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:needsReauthenticationDescription'} />
</AlertDescription>
</Alert>
);
}

View File

@@ -1,23 +0,0 @@
'use client';
import { useRevalidatePersonalAccountDataQuery } from '../../hooks/use-personal-account-data';
import { UpdateAccountDetailsForm } from './update-account-details-form';
export function UpdateAccountDetailsFormContainer({
user,
}: {
user: {
name: string | null;
id: string;
};
}) {
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
return (
<UpdateAccountDetailsForm
displayName={user.name ?? ''}
userId={user.id}
onUpdate={() => revalidateUserDataQuery(user.id)}
/>
);
}

View File

@@ -1,98 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Database } from '@kit/supabase/database';
import { Button } from '@kit/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { useUpdateAccountData } from '../../hooks/use-update-account';
import { AccountDetailsSchema } from '../../schema/account-details.schema';
type UpdateUserDataParams =
Database['medreport']['Tables']['accounts']['Update'];
export function UpdateAccountDetailsForm({
displayName,
onUpdate,
userId,
}: {
displayName: string;
userId: string;
onUpdate: (user: Partial<UpdateUserDataParams>) => void;
}) {
const updateAccountMutation = useUpdateAccountData(userId);
const { t } = useTranslation('account');
const form = useForm({
resolver: zodResolver(AccountDetailsSchema),
defaultValues: {
displayName,
},
});
const onSubmit = ({ displayName }: { displayName: string }) => {
const data = { name: displayName };
const promise = updateAccountMutation.mutateAsync(data).then(() => {
onUpdate(data);
});
return toast.promise(() => promise, {
success: t(`updateProfileSuccess`),
error: t(`updateProfileError`),
loading: t(`updateProfileLoading`),
});
};
return (
<div className={'flex flex-col space-y-8'}>
<Form {...form}>
<form
data-test={'update-account-name-form'}
className={'flex flex-col space-y-4'}
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name={'displayName'}
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'account:name'} />
</FormLabel>
<FormControl>
<Input
data-test={'account-display-name'}
minLength={2}
placeholder={''}
maxLength={100}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<Button disabled={updateAccountMutation.isPending}>
<Trans i18nKey={'account:updateProfileSubmitLabel'} />
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@@ -1,168 +0,0 @@
'use client';
import { useCallback } from 'react';
import type { SupabaseClient } from '@supabase/supabase-js';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Database } from '@kit/supabase/database';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { ImageUploader } from '@kit/ui/image-uploader';
import { Trans } from '@kit/ui/trans';
import { useRevalidatePersonalAccountDataQuery } from '../../hooks/use-personal-account-data';
const AVATARS_BUCKET = 'account_image';
export function UpdateAccountImageContainer({
user,
}: {
user: {
pictureUrl: string | null;
id: string;
};
}) {
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
return (
<UploadProfileAvatarForm
pictureUrl={user.pictureUrl ?? null}
userId={user.id}
onAvatarUpdated={() => revalidateUserDataQuery(user.id)}
/>
);
}
function UploadProfileAvatarForm(props: {
pictureUrl: string | null;
userId: string;
onAvatarUpdated: () => void;
}) {
const client = useSupabase();
const { t } = useTranslation('account');
const createToaster = useCallback(
(promise: () => Promise<unknown>) => {
return toast.promise(promise, {
success: t(`updateProfileSuccess`),
error: t(`updateProfileError`),
loading: t(`updateProfileLoading`),
});
},
[t],
);
const onValueChange = useCallback(
(file: File | null) => {
const removeExistingStorageFile = () => {
if (props.pictureUrl) {
return (
deleteProfilePhoto(client, props.pictureUrl) ?? Promise.resolve()
);
}
return Promise.resolve();
};
if (file) {
const promise = () =>
removeExistingStorageFile().then(() =>
uploadUserProfilePhoto(client, file, props.userId)
.then((pictureUrl) => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: pictureUrl,
})
.eq('id', props.userId)
.throwOnError();
})
.then(() => {
props.onAvatarUpdated();
}),
);
createToaster(promise);
} else {
const promise = () =>
removeExistingStorageFile()
.then(() => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: null,
})
.eq('id', props.userId)
.throwOnError();
})
.then(() => {
props.onAvatarUpdated();
});
createToaster(promise);
}
},
[client, createToaster, props],
);
return (
<ImageUploader value={props.pictureUrl} onValueChange={onValueChange}>
<div className={'flex flex-col space-y-1'}>
<span className={'text-sm'}>
<Trans i18nKey={'account:profilePictureHeading'} />
</span>
<span className={'text-xs'}>
<Trans i18nKey={'account:profilePictureSubheading'} />
</span>
</div>
</ImageUploader>
);
}
function deleteProfilePhoto(client: SupabaseClient<Database>, url: string) {
const bucket = client.storage.from(AVATARS_BUCKET);
const fileName = url.split('/').pop()?.split('?')[0];
if (!fileName) {
return;
}
return bucket.remove([fileName]);
}
async function uploadUserProfilePhoto(
client: SupabaseClient<Database>,
photoFile: File,
userId: string,
) {
const bytes = await photoFile.arrayBuffer();
const bucket = client.storage.from(AVATARS_BUCKET);
const extension = photoFile.name.split('.').pop();
const fileName = await getAvatarFileName(userId, extension);
const result = await bucket.upload(fileName, bytes);
if (!result.error) {
return bucket.getPublicUrl(fileName).data.publicUrl;
}
throw result.error;
}
async function getAvatarFileName(
userId: string,
extension: string | undefined,
) {
const { nanoid } = await import('nanoid');
// we add a version to the URL to ensure
// the browser always fetches the latest image
const uniqueId = nanoid(16);
return `${userId}.${extension}?v=${uniqueId}`;
}

View File

@@ -2,19 +2,19 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import {
AnalysisResultDetails,
UserAnalysis,
UserAnalysisResponse,
} from '../types/accounts';
import { AnalysisResultDetails, UserAnalysis } from '../types/accounts';
export type AccountWithParams =
Database['medreport']['Tables']['accounts']['Row'] & {
account_params:
| Pick<
accountParams:
| (Pick<
Database['medreport']['Tables']['account_params']['Row'],
'weight' | 'height'
>[]
> & {
isSmoker:
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
| null;
})
| null;
};
@@ -35,7 +35,9 @@ class AccountsApi {
const { data, error } = await this.client
.schema('medreport')
.from('accounts')
.select('*, account_params: account_params (weight, height)')
.select(
'*, accountParams: account_params (weight, height, isSmoker:is_smoker)',
)
.eq('id', id)
.single();

View File

@@ -32,9 +32,8 @@ const SPECIAL_CHARACTERS_REGEX = /[!@#$%^&*()+=[\]{};':"\\|,.<>/?]/;
* @name CompanyNameSchema
*/
export const CompanyNameSchema = z
.string({
description: 'The name of the company account',
})
.string()
.describe('The name of the company account')
.min(2)
.max(50)
.refine(

View File

@@ -410,7 +410,7 @@ export async function getAnalysisResultsForDoctor(
.from('accounts')
.select(
`primary_owner_user_id, id, name, last_name, personal_code, phone, email, preferred_locale,
account_params(height,weight)`,
accountParams:account_params(height,weight)`,
)
.eq('is_personal_account', true)
.eq('primary_owner_user_id', userId)
@@ -472,7 +472,7 @@ export async function getAnalysisResultsForDoctor(
last_name,
personal_code,
phone,
account_params,
accountParams,
preferred_locale,
} = accountWithParams[0];
@@ -513,8 +513,8 @@ export async function getAnalysisResultsForDoctor(
personalCode: personal_code,
phone,
email,
height: account_params?.[0]?.height,
weight: account_params?.[0]?.weight,
height: accountParams?.height,
weight: accountParams?.weight,
},
};
}

View File

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

View File

@@ -127,7 +127,7 @@ export async function login(_currentState: unknown, formData: FormData) {
}
}
export async function signout(countryCode: string) {
export async function signout(countryCode?: string, shouldRedirect = true) {
await sdk.auth.logout()
await removeAuthToken()
@@ -140,7 +140,9 @@ export async function signout(countryCode: string) {
const cartCacheTag = await getCacheTag("carts")
revalidateTag(cartCacheTag)
redirect(`/${countryCode}/account`)
if (shouldRedirect) {
redirect(`/${countryCode!}/account`)
}
}
export async function transferCart() {
@@ -272,7 +274,12 @@ export async function medusaLoginOrRegister(credentials: {
password,
});
await setAuthToken(token as string);
await transferCart();
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag);
@@ -307,7 +314,12 @@ export async function medusaLoginOrRegister(credentials: {
const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag);
await transferCart();
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customer = await retrieveCustomer();
if (!customer) {

View File

@@ -14,7 +14,7 @@ export const listProducts = async ({
regionId,
}: {
pageParam?: number
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string; id?: string[] }
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string; id?: string[], category_id?: string }
countryCode?: string
regionId?: string
}): Promise<{
@@ -63,7 +63,7 @@ export const listProducts = async ({
offset,
region_id: region?.id,
fields:
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags",
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,+status",
...queryParams,
},
headers,

View File

@@ -17,22 +17,22 @@ const env = z
.object({
invitePath: z
.string({
required_error: 'The property invitePath is required',
error: 'The property invitePath is required',
})
.min(1),
siteURL: z
.string({
required_error: 'NEXT_PUBLIC_SITE_URL is required',
error: 'NEXT_PUBLIC_SITE_URL is required',
})
.min(1),
productName: z
.string({
required_error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
})
.min(1),
emailSender: z
.string({
required_error: 'EMAIL_SENDER is required',
error: 'EMAIL_SENDER is required',
})
.min(1),
})

View File

@@ -78,7 +78,7 @@ class AccountWebhooksService {
productName: z.string(),
fromEmail: z
.string({
required_error: 'EMAIL_SENDER is required',
error: 'EMAIL_SENDER is required',
})
.min(1),
})

View File

@@ -8,9 +8,9 @@ type Config = z.infer<typeof MailerSchema>;
const RESEND_API_KEY = z
.string({
description: 'The API key for the Resend API',
required_error: 'Please provide the API key for the Resend API',
error: 'Please provide the API key for the Resend API',
})
.describe('The API key for the Resend API')
.parse(process.env.RESEND_API_KEY);
export function createResendMailer() {

View File

@@ -4,25 +4,19 @@ import { z } from 'zod';
export const SmtpConfigSchema = z.object({
user: z.string({
description:
'This is the email account to send emails from. This is specific to the email provider.',
required_error: `Please provide the variable EMAIL_USER`,
}),
error: `Please provide the variable EMAIL_USER`,
})
.describe('This is the email account to send emails from. This is specific to the email provider.'),
pass: z.string({
description: 'This is the password for the email account',
required_error: `Please provide the variable EMAIL_PASSWORD`,
}),
error: `Please provide the variable EMAIL_PASSWORD`,
}).describe('This is the password for the email account'),
host: z.string({
description: 'This is the SMTP host for the email provider',
required_error: `Please provide the variable EMAIL_HOST`,
}),
error: `Please provide the variable EMAIL_HOST`,
}).describe('This is the SMTP host for the email provider'),
port: z.number({
description:
'This is the port for the email provider. Normally 587 or 465.',
required_error: `Please provide the variable EMAIL_PORT`,
}),
error: `Please provide the variable EMAIL_PORT`,
}).describe('This is the port for the email provider. Normally 587 or 465.'),
secure: z.boolean({
description: 'This is whether the connection is secure or not',
required_error: `Please provide the variable EMAIL_TLS`,
}),
error: `Please provide the variable EMAIL_TLS`,
}).describe('This is whether the connection is secure or not'),
});

View File

@@ -4,9 +4,9 @@ import { MonitoringService } from '@kit/monitoring-core';
const apiKey = z
.string({
required_error: 'NEXT_PUBLIC_BASELIME_KEY is required',
description: 'The Baseline API key',
error: 'NEXT_PUBLIC_BASELIME_KEY is required',
})
.describe('The Baseline API key')
.parse(process.env.NEXT_PUBLIC_BASELIME_KEY);
export class BaselimeServerMonitoringService implements MonitoringService {

View File

@@ -6,14 +6,14 @@ import { getLogger } from '@kit/shared/logger';
const EMAIL_SENDER = z
.string({
required_error: 'EMAIL_SENDER is required',
error: 'EMAIL_SENDER is required',
})
.min(1)
.parse(process.env.EMAIL_SENDER);
const PRODUCT_NAME = z
.string({
required_error: 'PRODUCT_NAME is required',
error: 'PRODUCT_NAME is required',
})
.min(1)
.parse(process.env.NEXT_PUBLIC_PRODUCT_NAME);

View File

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

View File

@@ -21,6 +21,7 @@ import {
import { Trans } from '@kit/ui/trans';
import { ButtonTooltip } from './ui/button-tooltip';
import { PackageHeader } from './package-header';
import { pathsConfig } from '../config';
export type AnalysisPackageWithVariant = Pick<StoreProduct, 'title' | 'description' | 'subtitle' | 'metadata'> & {
variantId: string;
@@ -57,7 +58,7 @@ export default function SelectAnalysisPackage({
});
setIsAddingToCart(false);
toast.success(<Trans i18nKey={'order-analysis-package:analysisPackageAddedToCart'} />);
router.push('/home/cart');
router.push(pathsConfig.app.cart);
} catch (e) {
toast.error(<Trans i18nKey={'order-analysis-package:analysisPackageAddToCartError'} />);
setIsAddingToCart(false);

View File

@@ -0,0 +1,24 @@
'use client'
import { DropdownMenuItem } from "@kit/ui/dropdown-menu";
import { Trans } from "@kit/ui/trans";
import { LogOut } from "lucide-react";
export default function SignOutDropdownItem(
props: React.PropsWithChildren<{
onSignOut: () => unknown;
}>,
) {
return (
<DropdownMenuItem
className={'flex h-12 w-full items-center space-x-4'}
onClick={props.onSignOut}
>
<LogOut className={'h-6'} />
<span>
<Trans i18nKey={'common:signOut'} defaults={'Sign out'} />
</span>
</DropdownMenuItem>
);
}

View File

@@ -0,0 +1,33 @@
'use client'
import { DropdownMenuItem } from "@kit/ui/dropdown-menu";
import { Trans } from "@kit/ui/trans";
import Link from "next/link";
export default function DropdownLink(
props: React.PropsWithChildren<{
path: string;
label: string;
labelOptions?: Record<string, any>;
Icon?: React.ReactNode;
}>,
) {
return (
<DropdownMenuItem asChild key={props.path}>
<Link
href={props.path}
className={'flex h-12 w-full items-center space-x-4'}
>
{props.Icon}
<span>
<Trans
i18nKey={props.label}
defaults={props.label}
values={props.labelOptions}
/>
</span>
</Link>
</DropdownMenuItem>
);
}

View File

@@ -13,7 +13,7 @@ export function InfoTooltip({
content,
icon,
}: {
content?: string | null;
content?: JSX.Element | string | null;
icon?: JSX.Element;
}) {
if (!content) return null;
@@ -23,7 +23,7 @@ export function InfoTooltip({
<TooltipTrigger>
{icon || <Info className="size-4 cursor-pointer" />}
</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
<TooltipContent className='sm:max-w-[400px]'>{content}</TooltipContent>
</Tooltip>
</TooltipProvider>
);

View File

@@ -6,32 +6,30 @@ const AppConfigSchema = z
.object({
name: z
.string({
description: `This is the name of your SaaS. Ex. "Makerkit"`,
required_error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
})
.describe(`This is the name of your SaaS. Ex. "Makerkit"`)
.min(1),
title: z
.string({
description: `This is the default title tag of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
})
.describe(`This is the default title tag of your SaaS.`)
.min(1),
description: z.string({
description: `This is the default description of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`,
error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`,
})
.describe(`This is the default description of your SaaS.`),
url: z.url({
error: (issue) => issue.input === undefined
? "Please provide the variable NEXT_PUBLIC_SITE_URL"
: `You are deploying a production build but have entered a NEXT_PUBLIC_SITE_URL variable using http instead of https. It is very likely that you have set the incorrect URL. The build will now fail to prevent you from from deploying a faulty configuration. Please provide the variable NEXT_PUBLIC_SITE_URL with a valid URL, such as: 'https://example.com'`
}),
url: z
.string({
required_error: `Please provide the variable NEXT_PUBLIC_SITE_URL`,
})
.url({
message: `You are deploying a production build but have entered a NEXT_PUBLIC_SITE_URL variable using http instead of https. It is very likely that you have set the incorrect URL. The build will now fail to prevent you from from deploying a faulty configuration. Please provide the variable NEXT_PUBLIC_SITE_URL with a valid URL, such as: 'https://example.com'`,
}),
locale: z
.string({
description: `This is the default locale of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
})
.describe(`This is the default locale of your SaaS.`)
.default('en'),
theme: z.enum(['light', 'dark', 'system']),
production: z.boolean(),

View File

@@ -6,22 +6,14 @@ const providers: z.ZodType<Provider> = getProviders();
const AuthConfigSchema = z.object({
captchaTokenSiteKey: z
.string({
description: 'The reCAPTCHA site key.',
})
.string().describe('The reCAPTCHA site key.')
.optional(),
displayTermsCheckbox: z
.boolean({
description: 'Whether to display the terms checkbox during sign-up.',
})
.boolean().describe('Whether to display the terms checkbox during sign-up.')
.optional(),
providers: z.object({
password: z.boolean({
description: 'Enable password authentication.',
}),
magicLink: z.boolean({
description: 'Enable magic link authentication.',
}),
password: z.boolean().describe('Enable password authentication.'),
magicLink: z.boolean().describe('Enable magic link authentication.'),
oAuth: providers.array(),
}),
});

View File

@@ -4,56 +4,56 @@ type LanguagePriority = 'user' | 'application';
const FeatureFlagsSchema = z.object({
enableThemeToggle: z.boolean({
description: 'Enable theme toggle in the user interface.',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
}),
error: 'Provide the variable NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
})
.describe( 'Enable theme toggle in the user interface.'),
enableAccountDeletion: z.boolean({
description: 'Enable personal account deletion.',
required_error:
error:
'Provide the variable NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
}),
})
.describe('Enable personal account deletion.'),
enableTeamDeletion: z.boolean({
description: 'Enable team deletion.',
required_error:
error:
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
}),
})
.describe('Enable team deletion.'),
enableTeamAccounts: z.boolean({
description: 'Enable team accounts.',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
}),
error: 'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
})
.describe('Enable team accounts.'),
enableTeamCreation: z.boolean({
description: 'Enable team creation.',
required_error:
error:
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
}),
})
.describe('Enable team creation.'),
enablePersonalAccountBilling: z.boolean({
description: 'Enable personal account billing.',
required_error:
error:
'Provide the variable NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
}),
})
.describe('Enable personal account billing.'),
enableTeamAccountBilling: z.boolean({
description: 'Enable team account billing.',
required_error:
error:
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
}),
})
.describe('Enable team account billing.'),
languagePriority: z
.enum(['user', 'application'], {
required_error: 'Provide the variable NEXT_PUBLIC_LANGUAGE_PRIORITY',
description: `If set to user, use the user's preferred language. If set to application, use the application's default language.`,
error: 'Provide the variable NEXT_PUBLIC_LANGUAGE_PRIORITY',
})
.describe(`If set to user, use the user's preferred language. If set to application, use the application's default language.`)
.default('application'),
enableNotifications: z.boolean({
description: 'Enable notifications functionality',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
}),
error: 'Provide the variable NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
})
.describe('Enable notifications functionality'),
realtimeNotifications: z.boolean({
description: 'Enable realtime for the notifications functionality',
required_error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
}),
error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
})
.describe('Enable realtime for the notifications functionality'),
enableVersionUpdater: z.boolean({
description: 'Enable version updater',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_VERSION_UPDATER',
}),
error: 'Provide the variable NEXT_PUBLIC_ENABLE_VERSION_UPDATER',
})
.describe('Enable version updater'),
});
const featureFlagsConfig = FeatureFlagsSchema.parse({

View File

@@ -14,6 +14,7 @@ const PathsSchema = z.object({
}),
app: z.object({
home: z.string().min(1),
cart: z.string().min(1),
selectPackage: z.string().min(1),
booking: z.string().min(1),
bookingHandle: z.string().min(1),
@@ -23,6 +24,8 @@ const PathsSchema = z.object({
orderAnalysis: z.string().min(1),
orderHealthAnalysis: z.string().min(1),
personalAccountSettings: z.string().min(1),
personalAccountPreferences: z.string().min(1),
personalAccountSecurity: z.string().min(1),
personalAccountBilling: z.string().min(1),
personalAccountBillingReturn: z.string().min(1),
accountHome: z.string().min(1),
@@ -54,7 +57,10 @@ const pathsConfig = PathsSchema.parse({
},
app: {
home: '/home',
cart: '/home/cart',
personalAccountSettings: '/home/settings',
personalAccountPreferences: '/home/settings/preferences',
personalAccountSecurity: '/home/settings/security',
personalAccountBilling: '/home/billing',
personalAccountBillingReturn: '/home/billing/return',
accountHome: '/home/[account]',

View File

@@ -5,7 +5,6 @@ import {
MousePointerClick,
ShoppingCart,
Stethoscope,
TestTube2,
} from 'lucide-react';
import { z } from 'zod';

View File

@@ -199,7 +199,6 @@ export type Database = {
changed_by: string
created_at: string
id: number
extra_data?: Json | null
}
Insert: {
account_id: string
@@ -207,7 +206,6 @@ export type Database = {
changed_by: string
created_at?: string
id?: number
extra_data?: Json | null
}
Update: {
account_id?: string
@@ -215,7 +213,6 @@ export type Database = {
changed_by?: string
created_at?: string
id?: number
extra_data?: Json | null
}
Relationships: []
}
@@ -320,10 +317,10 @@ export type Database = {
Functions: {
graphql: {
Args: {
extensions?: Json
operationName?: string
query?: string
variables?: Json
extensions?: Json
}
Returns: Json
}
@@ -342,6 +339,7 @@ export type Database = {
account_id: string
height: number | null
id: string
is_smoker: boolean | null
recorded_at: string
weight: number | null
}
@@ -349,6 +347,7 @@ export type Database = {
account_id?: string
height?: number | null
id?: string
is_smoker?: boolean | null
recorded_at?: string
weight?: number | null
}
@@ -356,6 +355,7 @@ export type Database = {
account_id?: string
height?: number | null
id?: string
is_smoker?: boolean | null
recorded_at?: string
weight?: number | null
}
@@ -363,21 +363,21 @@ export type Database = {
{
foreignKeyName: "account_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
isOneToOne: true
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "account_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
isOneToOne: true
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "account_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
isOneToOne: true
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
@@ -1091,7 +1091,7 @@ export type Database = {
price: number
price_periods: string | null
requires_payment: boolean
sync_id: string | null
sync_id: string
updated_at: string | null
}
Insert: {
@@ -1110,7 +1110,7 @@ export type Database = {
price: number
price_periods?: string | null
requires_payment: boolean
sync_id?: string | null
sync_id: string
updated_at?: string | null
}
Update: {
@@ -1129,7 +1129,7 @@ export type Database = {
price?: number
price_periods?: string | null
requires_payment?: boolean
sync_id?: string | null
sync_id?: string
updated_at?: string | null
}
Relationships: [
@@ -1150,7 +1150,7 @@ export type Database = {
doctor_user_id: string | null
id: number
status: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at: string | null
updated_at: string
updated_by: string | null
user_id: string
value: string | null
@@ -1162,7 +1162,7 @@ export type Database = {
doctor_user_id?: string | null
id?: number
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at?: string | null
updated_at?: string
updated_by?: string | null
user_id: string
value?: string | null
@@ -1174,7 +1174,7 @@ export type Database = {
doctor_user_id?: string | null
id?: number
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at?: string | null
updated_at?: string
updated_by?: string | null
user_id?: string
value?: string | null
@@ -1257,34 +1257,6 @@ export type Database = {
},
]
}
medipost_actions: {
Row: {
id: string
action: string
xml: string
has_analysis_results: boolean
created_at: string
medusa_order_id: string
response_xml: string
has_error: boolean
}
Insert: {
action: string
xml: string
has_analysis_results: boolean
medusa_order_id: string
response_xml: string
has_error: boolean
}
Update: {
action?: string
xml?: string
has_analysis_results?: boolean
medusa_order_id?: string
response_xml?: string
has_error?: boolean
}
}
medreport_product_groups: {
Row: {
created_at: string
@@ -1871,17 +1843,19 @@ export type Database = {
}
create_nonce: {
Args: {
p_user_id?: string
p_purpose?: string
p_expires_in_seconds?: number
p_metadata?: Json
p_scopes?: string[]
p_purpose?: string
p_revoke_previous?: boolean
p_scopes?: string[]
p_user_id?: string
}
Returns: Json
}
create_team_account: {
Args: { account_name: string; new_personal_code: string }
Args:
| { account_name: string }
| { account_name: string; new_personal_code: string }
Returns: {
application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null
@@ -1908,34 +1882,34 @@ export type Database = {
get_account_invitations: {
Args: { account_slug: string }
Returns: {
id: number
email: string
account_id: string
invited_by: string
role: string
created_at: string
updated_at: string
email: string
expires_at: string
personal_code: string
inviter_name: string
id: number
invited_by: string
inviter_email: string
inviter_name: string
personal_code: string
role: string
updated_at: string
}[]
}
get_account_members: {
Args: { account_slug: string }
Returns: {
id: string
user_id: string
account_id: string
role: string
role_hierarchy_level: number
primary_owner_user_id: string
name: string
created_at: string
email: string
id: string
name: string
personal_code: string
picture_url: string
created_at: string
primary_owner_user_id: string
role: string
role_hierarchy_level: number
updated_at: string
user_id: string
}[]
}
get_config: {
@@ -1945,20 +1919,11 @@ export type Database = {
get_invitations_with_account_ids: {
Args: { company_id: string; personal_codes: string[] }
Returns: {
account_id: string
invite_token: string
personal_code: string
account_id: string
}[]
}
get_latest_medipost_dispatch_state_for_order: {
Args: {
medusa_order_id: string
}
Returns: {
has_success: boolean
action_date: string
}
}
get_medipost_dispatch_tries: {
Args: { p_medusa_order_id: string }
Returns: number
@@ -1985,17 +1950,17 @@ export type Database = {
}
has_more_elevated_role: {
Args: {
target_user_id: string
target_account_id: string
role_name: string
target_account_id: string
target_user_id: string
}
Returns: boolean
}
has_permission: {
Args: {
user_id: string
account_id: string
permission_name: Database["medreport"]["Enums"]["app_permissions"]
user_id: string
}
Returns: boolean
}
@@ -2005,9 +1970,9 @@ export type Database = {
}
has_same_role_hierarchy_level: {
Args: {
target_user_id: string
target_account_id: string
role_name: string
target_account_id: string
target_user_id: string
}
Returns: boolean
}
@@ -2062,39 +2027,39 @@ export type Database = {
team_account_workspace: {
Args: { account_slug: string }
Returns: {
id: string
name: string
picture_url: string
slug: string
role: string
role_hierarchy_level: number
primary_owner_user_id: string
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
permissions: Database["medreport"]["Enums"]["app_permissions"][]
account_role: string
application_role: Database["medreport"]["Enums"]["application_role"]
id: string
name: string
permissions: Database["medreport"]["Enums"]["app_permissions"][]
picture_url: string
primary_owner_user_id: string
role: string
role_hierarchy_level: number
slug: string
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
}[]
}
transfer_team_account_ownership: {
Args: { target_account_id: string; new_owner_id: string }
Args: { new_owner_id: string; target_account_id: string }
Returns: undefined
}
update_account: {
Args: {
p_name: string
p_last_name: string
p_personal_code: string
p_phone: string
p_city: string
p_has_consent_personal_data: boolean
p_last_name: string
p_name: string
p_personal_code: string
p_phone: string
p_uid: string
}
Returns: undefined
}
update_analysis_order_status: {
Args: {
order_id: number
medusa_order_id_param: string
order_id: number
status_param: Database["medreport"]["Enums"]["analysis_order_status"]
}
Returns: {
@@ -2109,14 +2074,14 @@ export type Database = {
}
upsert_order: {
Args: {
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
currency: string
line_items: Json
status: Database["medreport"]["Enums"]["payment_status"]
target_account_id: string
target_customer_id: string
target_order_id: string
status: Database["medreport"]["Enums"]["payment_status"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
total_amount: number
currency: string
line_items: Json
}
Returns: {
account_id: string
@@ -2132,19 +2097,19 @@ export type Database = {
}
upsert_subscription: {
Args: {
target_account_id: string
target_customer_id: string
target_subscription_id: string
active: boolean
status: Database["medreport"]["Enums"]["subscription_status"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
cancel_at_period_end: boolean
currency: string
period_starts_at: string
period_ends_at: string
line_items: Json
trial_starts_at?: string
period_ends_at: string
period_starts_at: string
status: Database["medreport"]["Enums"]["subscription_status"]
target_account_id: string
target_customer_id: string
target_subscription_id: string
trial_ends_at?: string
trial_starts_at?: string
}
Returns: {
account_id: string
@@ -2165,31 +2130,16 @@ export type Database = {
}
verify_nonce: {
Args: {
p_token: string
p_purpose: string
p_user_id?: string
p_required_scopes?: string[]
p_max_verification_attempts?: number
p_ip?: unknown
p_max_verification_attempts?: number
p_purpose: string
p_required_scopes?: string[]
p_token: string
p_user_agent?: string
p_user_id?: string
}
Returns: Json
}
sync_analysis_results: {
}
send_medipost_test_response_for_order: {
Args: {
medusa_order_id: string
}
}
order_has_medipost_dispatch_error: {
Args: {
medusa_order_id: string
}
Returns: {
success: boolean
}
}
}
Enums: {
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"
@@ -7918,9 +7868,9 @@ export type Database = {
Functions: {
has_permission: {
Args: {
user_id: string
account_id: string
permission_name: Database["public"]["Enums"]["app_permissions"]
user_id: string
}
Returns: boolean
}
@@ -7970,21 +7920,25 @@ export type Database = {
}
}
type DefaultSchema = Database[Extract<keyof Database, "public">]
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
| { schema: keyof Database },
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
schema: keyof DatabaseWithoutInternals
}
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
: never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
@@ -8002,14 +7956,16 @@ export type Tables<
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof Database },
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
schema: keyof DatabaseWithoutInternals
}
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
@@ -8025,14 +7981,16 @@ export type TablesInsert<
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof Database },
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
schema: keyof DatabaseWithoutInternals
}
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
@@ -8048,14 +8006,16 @@ export type TablesUpdate<
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema["Enums"]
| { schema: keyof Database },
| { schema: keyof DatabaseWithoutInternals },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof Database
schema: keyof DatabaseWithoutInternals
}
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
> = DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never
@@ -8063,14 +8023,16 @@ export type Enums<
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema["CompositeTypes"]
| { schema: keyof Database },
| { schema: keyof DatabaseWithoutInternals },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof Database
schema: keyof DatabaseWithoutInternals
}
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never,
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
> = PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never

View File

@@ -13,7 +13,7 @@ const message =
export function getServiceRoleKey() {
return z
.string({
required_error: message,
error: message,
})
.min(1, {
message: message,

View File

@@ -7,14 +7,14 @@ export function getSupabaseClientKeys() {
return z
.object({
url: z.string({
description: `This is the URL of your hosted Supabase instance. Please provide the variable NEXT_PUBLIC_SUPABASE_URL.`,
required_error: `Please provide the variable NEXT_PUBLIC_SUPABASE_URL`,
}),
error: `Please provide the variable NEXT_PUBLIC_SUPABASE_URL`,
})
.describe(`This is the URL of your hosted Supabase instance. Please provide the variable NEXT_PUBLIC_SUPABASE_URL.`),
anonKey: z
.string({
description: `This is the anon key provided by Supabase. It is a public key used client-side. Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY.`,
required_error: `Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY`,
error: `Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY`,
})
.describe(`This is the anon key provided by Supabase. It is a public key used client-side. Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY.`)
.min(1),
})
.parse({

View File

@@ -1,12 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
import { signout } from '../../../features/medusa-storefront/src/lib/data/customer';
export function useSignOut() {
const client = useSupabase();
return useMutation({
mutationFn: () => {
mutationFn: async () => {
await signout(undefined, false);
return client.auth.signOut();
},
});

View File

@@ -14,7 +14,7 @@ export function useUser(initialData?: User | null) {
// this is most likely a session error or the user is not logged in
if (response.error) {
return undefined;
return null;
}
if (response.data?.user) {

View File

@@ -1,7 +1,10 @@
import { z } from 'zod';
const RouteMatchingEnd = z
.union([z.boolean(), z.function().args(z.string()).returns(z.boolean())])
.union([
z.boolean(),
z.function({ input: [z.string()], output: z.boolean() }),
])
.default(false)
.optional();

3809
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +1,169 @@
{
"accountTabLabel": "Account Settings",
"accountTabDescription": "Manage your account settings",
"accountTabLabel": "Account settings",
"accountTabDescription": "Manage your account settings and email preferences.",
"preferencesTabLabel": "Preferences",
"preferencesTabDescription": "Manage your preferences.",
"securityTabLabel": "Security",
"securityTabDescription": "Protect your account.",
"homePage": "Home",
"billingTab": "Billing",
"settingsTab": "Settings",
"multiFactorAuth": "Multi-Factor Authentication",
"multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account",
"multiFactorAuth": "Multi-factor authentication",
"multiFactorAuthDescription": "Set up multi-factor authentication to better protect your account",
"updateProfileSuccess": "Profile successfully updated",
"updateProfileError": "Encountered an error. Please try again",
"updatePasswordSuccess": "Password update request successful",
"updateProfileError": "An error occurred. Please try again",
"updatePasswordSuccess": "Password update successful",
"updatePasswordSuccessMessage": "Your password has been successfully updated!",
"updatePasswordError": "Encountered an error. Please try again",
"updatePasswordError": "An error occurred. Please try again",
"updatePasswordLoading": "Updating password...",
"updateProfileLoading": "Updating profile...",
"name": "Your Name",
"nameDescription": "Update your name to be displayed on your profile",
"emailLabel": "Email Address",
"accountImage": "Your Profile Picture",
"accountImageDescription": "Please choose a photo to upload as your profile picture.",
"profilePictureHeading": "Upload a Profile Picture",
"name": "Your name",
"nameDescription": "Update the name displayed on your profile",
"emailLabel": "Email address",
"accountImage": "Your profile picture",
"accountImageDescription": "Choose a photo to upload as your profile picture.",
"profilePictureHeading": "Upload a profile picture",
"profilePictureSubheading": "Choose a photo to upload as your profile picture.",
"updateProfileSubmitLabel": "Update Profile",
"updatePasswordCardTitle": "Update your Password",
"updateProfileSubmitLabel": "Update profile",
"updatePasswordCardTitle": "Update your password",
"updatePasswordCardDescription": "Update your password to keep your account secure.",
"currentPassword": "Current Password",
"newPassword": "New Password",
"repeatPassword": "Repeat New Password",
"currentPassword": "Current password",
"newPassword": "New password",
"repeatPassword": "Repeat new password",
"repeatPasswordDescription": "Please repeat your new password to confirm it",
"yourPassword": "Your Password",
"updatePasswordSubmitLabel": "Update Password",
"updateEmailCardTitle": "Update your Email",
"updateEmailCardDescription": "Update your email address you use to login to your account",
"newEmail": "Your New Email",
"repeatEmail": "Repeat Email",
"updateEmailSubmitLabel": "Update Email Address",
"updateEmailSuccess": "Email update request successful",
"updateEmailSuccessMessage": "We sent you an email to confirm your new email address. Please check your inbox and click on the link to confirm your new email address.",
"updateEmailLoading": "Updating your email...",
"yourPassword": "Your password",
"updatePasswordSubmitLabel": "Update password",
"updateEmailCardTitle": "Update your email",
"updateEmailCardDescription": "Update the email address you use to log in",
"newEmail": "Your new email",
"repeatEmail": "Repeat email",
"updateEmailSubmitLabel": "Update email address",
"updateEmailSuccess": "Email update successful",
"updateEmailSuccessMessage": "We will send you a confirmation email to verify your new address. Please check your inbox and click the link.",
"updateEmailLoading": "Updating email...",
"updateEmailError": "Email not updated. Please try again",
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
"emailNotMatching": "Emails do not match. Make sure you're using the correct email",
"passwordNotChanged": "Your password has not changed",
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
"cannotUpdatePassword": "You cannot update your password because your account is not linked to any.",
"setupMfaButtonLabel": "Setup a new Factor",
"multiFactorSetupErrorHeading": "Setup Failed",
"multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.",
"multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication",
"multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.",
"factorNameLabel": "A memorable name to identify this factor",
"factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14",
"passwordNotMatching": "Passwords do not match. Make sure you are using the correct password",
"emailNotMatching": "Emails do not match. Make sure you are using the correct email",
"passwordNotChanged": "Your password has not been changed",
"emailsNotMatching": "Emails do not match. Make sure you are using the correct email",
"cannotUpdatePassword": "You cannot update your password because your account is not linked to a password.",
"setupMfaButtonLabel": "Set up new factor",
"multiFactorSetupErrorHeading": "Setup failed",
"multiFactorSetupErrorDescription": "Sorry, an error occurred while setting up the factor. Please try again.",
"multiFactorAuthHeading": "Protect your account with multi-factor authentication",
"multiFactorModalHeading": "Use your authentication app to scan the QR code. Then enter the generated code.",
"factorNameLabel": "Memorable name for factor identification",
"factorNameHint": "Use a simple name to easily identify this factor later. E.g. iPhone 14",
"factorNameSubmitLabel": "Set factor name",
"unenrollTooltip": "Unenroll this factor",
"unenrollingFactor": "Unenrolling factor...",
"unenrollFactorSuccess": "Factor successfully unenrolled",
"unenrollFactorError": "Unenrolling factor failed",
"unenrollTooltip": "Unregister this factor",
"unenrollingFactor": "Unregistering factor...",
"unenrollFactorSuccess": "Factor successfully removed",
"unenrollFactorError": "Failed to remove factor",
"factorsListError": "Error loading factors list",
"factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.",
"factorName": "Factor Name",
"factorsListErrorDescription": "Sorry, we could not load the factors list. Please try again.",
"factorName": "Factor name",
"factorType": "Type",
"factorStatus": "Status",
"mfaEnabledSuccessTitle": "Multi-Factor authentication is enabled",
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
"verificationCode": "Verification Code",
"addEmailAddress": "Add Email address",
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above",
"mfaEnabledSuccessTitle": "Multi-factor authentication enabled",
"mfaEnabledSuccessDescription": "Congratulations! You have successfully registered for multi-factor authentication. You can now log in with your password and authentication code.",
"verificationCode": "Verification code",
"addEmailAddress": "Add email address",
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authentication app",
"loadingFactors": "Loading factors...",
"enableMfaFactor": "Enable Factor",
"disableMfaFactor": "Disable Factor",
"qrCodeErrorHeading": "QR Code Error",
"qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code",
"multiFactorSetupSuccess": "Factor successfully enrolled",
"submitVerificationCode": "Submit Verification Code",
"mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled",
"enableMfaFactor": "Enable factor",
"disableMfaFactor": "Disable factor",
"qrCodeErrorHeading": "QR code error",
"qrCodeErrorDescription": "Sorry, QR code generation failed",
"multiFactorSetupSuccess": "Factor successfully registered",
"submitVerificationCode": "Submit verification code",
"mfaEnabledSuccessAlert": "Multi-factor authentication enabled",
"verifyingCode": "Verifying code...",
"invalidVerificationCodeHeading": "Invalid Verification Code",
"invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.",
"unenrollFactorModalHeading": "Unenroll Factor",
"unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
"unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
"unenrollFactorModalButtonLabel": "Yes, unenroll factor",
"selectFactor": "Choose a factor to verify your identity",
"disableMfa": "Disable Multi-Factor Authentication",
"invalidVerificationCodeHeading": "Invalid verification code",
"invalidVerificationCodeDescription": "The entered verification code is not valid. Please try again.",
"unenrollFactorModalHeading": "Unregister factor",
"unenrollFactorModalDescription": "You are about to unregister this factor. You will no longer be able to use it for login.",
"unenrollFactorModalBody": "You are about to unregister this factor. You will no longer be able to use it for login.",
"unenrollFactorModalButtonLabel": "Yes, remove factor",
"selectFactor": "Select a factor to verify your identity",
"disableMfa": "Disable multi-factor authentication",
"disableMfaButtonLabel": "Disable MFA",
"confirmDisableMfaButtonLabel": "Yes, disable MFA",
"disablingMfa": "Disabling Multi-Factor Authentication. Please wait...",
"disableMfaSuccess": "Multi-Factor Authentication successfully disabled",
"disableMfaError": "Sorry, we encountered an error. MFA has not been disabled.",
"sendingEmailVerificationLink": "Sending Email...",
"sendEmailVerificationLinkSuccess": "Verification link successfully sent",
"sendEmailVerificationLinkError": "Sorry, we weren't able to send you the email",
"sendVerificationLinkSubmitLabel": "Send Verification Link",
"sendVerificationLinkSuccessLabel": "Email sent! Check your Inbox",
"disablingMfa": "Disabling multi-factor authentication. Please wait...",
"disableMfaSuccess": "Multi-factor authentication successfully disabled",
"disableMfaError": "Sorry, an error occurred. MFA was not disabled.",
"sendingEmailVerificationLink": "Sending email...",
"sendEmailVerificationLinkSuccess": "Confirmation link successfully sent",
"sendEmailVerificationLinkError": "Sorry, sending email failed",
"sendVerificationLinkSubmitLabel": "Send confirmation link",
"sendVerificationLinkSuccessLabel": "Email sent! Check your inbox",
"verifyEmailAlertHeading": "Please verify your email to enable MFA",
"verificationLinkAlertDescription": "Your email is not yet verified. Please verify your email to be able to set up Multi-Factor Authentication.",
"authFactorName": "Factor Name (optional)",
"authFactorNameHint": "Assign a name that helps you remember the phone number used",
"loadingUser": "Loading user details. Please wait...",
"linkPhoneNumber": "Link Phone Number",
"dangerZone": "Danger Zone",
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.",
"deleteAccount": "Delete your Account",
"verificationLinkAlertDescription": "Your email has not yet been verified. Please confirm your email to set up multi-factor authentication.",
"authFactorName": "Factor name (optional)",
"authFactorNameHint": "Set a name to help remember the phone number used",
"loadingUser": "Loading user data. Please wait...",
"linkPhoneNumber": "Link phone number",
"dangerZone": "Danger zone",
"dangerZoneDescription": "Some actions cannot be undone. Be careful.",
"deleteAccount": "Delete your account",
"deletingAccount": "Deleting account. Please wait...",
"deleteAccountDescription": "This will delete your account and the accounts you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone.",
"deleteAccountDescription": "This will delete your account and all accounts you own. All active subscriptions will also be immediately canceled. This action cannot be undone.",
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
"deleteAccountErrorHeading": "Sorry, we couldn't delete your account",
"needsReauthentication": "Reauthentication Required",
"needsReauthenticationDescription": "You need to reauthenticate to change your password. Please sign out and sign in again to change your password.",
"deleteAccountErrorHeading": "Sorry, we could not delete your account",
"needsReauthentication": "Re-authentication required",
"needsReauthenticationDescription": "You must re-authenticate to change your password. Please log out and back in to change it.",
"language": "Language",
"languageDescription": "Choose your preferred language",
"noTeamsYet": "You don't have any teams yet.",
"noTeamsYet": "You dont have any teams yet.",
"createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account",
"createTeamButtonLabel": "Create team",
"createCompanyAccount": "Create a company account",
"requestCompanyAccount": {
"title": "Company details"
"title": "Company details",
"description": "To get an offer, please enter the company details you intend to use MedReport with.",
"button": "Request an offer",
"successTitle": "Request successfully sent!",
"successDescription": "We will get back to you as soon as possible",
"successButton": "Back to homepage"
},
"updateConsentSuccess": "Consent successfully updated",
"updateConsentError": "Encountered an error. Please try again",
"updateConsentLoading": "Updating consent...",
"updateAccount": {
"title": "Personal details",
"description": "Please enter your personal details to continue",
"button": "Continue",
"userConsentLabel": "I agree to the use of personal data on the platform",
"userConsentUrlTitle": "View privacy policy"
},
"consentModal": {
"title": "Before we start",
"description": "Do you consent to your health data being used anonymously in employer statistics? The data remains anonymized and helps companies better support employee health.",
"reject": "Do not consent",
"accept": "Consent"
},
"updateConsentSuccess": "Consents updated",
"updateConsentError": "Something went wrong. Please try again",
"updateConsentLoading": "Updating consents...",
"consentToAnonymizedCompanyData": {
"label": "Consent to be included in employer statistics",
"description": "Consent to be included in anonymized company statistics"
"label": "I agree to participate in employer statistics",
"description": "I agree to the use of anonymized health data in employer statistics"
},
"membershipConfirmation": {
"successTitle": "Hello, {{firstName}} {{lastName}}",
"successDescription": "Your health account has been activated and is ready to use!",
"successButton": "Continue"
},
"updateRoleSuccess": "Role updated",
"updateRoleError": "Something went wrong, please try again",
"updateRoleError": "Something went wrong. Please try again",
"updateRoleLoading": "Updating role...",
"updatePreferredLocaleSuccess": "Language preference updated",
"updatePreferredLocaleError": "Language preference update failed",
"updatePreferredLocaleLoading": "Updating language preference...",
"doctorAnalysisSummary": "Doctor's summary"
"updatePreferredLocaleSuccess": "Preferred language updated",
"updatePreferredLocaleError": "Failed to update preferred language",
"updatePreferredLocaleLoading": "Updating preferred language...",
"doctorAnalysisSummary": "Doctors summary of test results",
"myHabits": "My health habits",
"formField": {
"smoking": "I smoke"
},
"updateAccountSuccess": "Account details updated",
"updateAccountError": "Updating account details failed",
"updateAccountPreferencesSuccess": "Account preferences updated",
"updateAccountPreferencesError": "Updating account preferences failed",
"consents": "Consents"
}

View File

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

View File

@@ -1,15 +1,15 @@
{
"homeTabLabel": "Home",
"homeTabDescription": "Welcome to your home page",
"homeTabDescription": "Welcome to your homepage",
"accountMembers": "Company Members",
"membersTabDescription": "Here you can manage the members of your company.",
"membersTabDescription": "Here you can manage your company members.",
"billingTabLabel": "Billing",
"billingTabDescription": "Manage your billing and subscription",
"billingTabDescription": "Manage your billing and subscriptions",
"dashboardTabLabel": "Dashboard",
"settingsTabLabel": "Settings",
"profileSettingsTabLabel": "Profile",
"subscriptionSettingsTabLabel": "Subscription",
"dashboardTabDescription": "An overview of your account's activity and performance across all your projects.",
"dashboardTabDescription": "Overview of your account activity and project results.",
"settingsTabDescription": "Manage your settings and preferences.",
"emailAddress": "Email Address",
"password": "Password",
@@ -18,69 +18,72 @@
"cancel": "Cancel",
"clear": "Clear",
"close": "Close",
"notFound": "Not Found",
"backToHomePage": "Back to Home Page",
"goBack": "Go Back",
"notFound": "Not found",
"backToHomePage": "Back to homepage",
"goBack": "Go back",
"genericServerError": "Sorry, something went wrong.",
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
"pageNotFound": "Sorry, this page does not exist.",
"pageNotFoundSubHeading": "Apologies, the page you were looking for was not found",
"pageNotFoundSubHeading": "Sorry, the page you were looking for was not found",
"genericError": "Sorry, something went wrong.",
"genericErrorSubHeading": "Apologies, an error occurred while processing your request. Please contact us if the issue persists.",
"genericErrorSubHeading": "An error occurred while processing your request. Please contact us if the issue persists.",
"anonymousUser": "Anonymous",
"tryAgain": "Try Again",
"tryAgain": "Try again",
"theme": "Theme",
"lightTheme": "Light",
"darkTheme": "Dark",
"systemTheme": "System",
"expandSidebar": "Expand Sidebar",
"collapseSidebar": "Collapse Sidebar",
"expandSidebar": "Expand sidebar",
"collapseSidebar": "Collapse sidebar",
"documentation": "Documentation",
"getStarted": "Get Started",
"getStartedWithPlan": "Get Started with {{plan}}",
"getStarted": "Get started!",
"getStartedWithPlan": "Get started with plan {{plan}}",
"retry": "Retry",
"contactUs": "Contact Us",
"contactUs": "Contact us",
"loading": "Loading. Please wait...",
"yourAccounts": "Your Accounts",
"yourAccounts": "Your accounts",
"continue": "Continue",
"skip": "Skip",
"signedInAs": "Signed in as",
"pageOfPages": "Page {{page}} of {{total}}",
"noData": "No data available",
"pageNotFoundHeading": "Ouch! :|",
"errorPageHeading": "Ouch! :|",
"pageOfPages": "Page {{page}} / {{total}}",
"noData": "No data",
"pageNotFoundHeading": "Oops! :|",
"errorPageHeading": "Oops! :|",
"notifications": "Notifications",
"noNotifications": "No notifications",
"justNow": "Just now",
"newVersionAvailable": "New version available",
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
"newVersionSubmitButton": "Reload and Update",
"newVersionAvailableDescription": "A new version of the app is available. We recommend refreshing the page to get the latest updates and avoid issues.",
"newVersionSubmitButton": "Refresh and update",
"back": "Back",
"welcome": "Welcome",
"shoppingCart": "Shopping cart",
"shoppingCartCount": "Shopping cart ({{count}})",
"shoppingCart": "Shopping Cart",
"shoppingCartCount": "Shopping Cart ({{count}})",
"search": "Search{{end}}",
"myActions": "My actions",
"healthPackageComparison": {
"label": "Health package comparison",
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
"label": "Health Package Comparison",
"description": "Based on preliminary data (gender, age, and BMI), we suggest a personalized health audit package. In the table, you can add additional tests to the recommended package."
},
"routes": {
"home": "Home",
"overview": "Overview",
"booking": "Booking",
"booking": "Book appointment",
"myOrders": "My orders",
"analysisResults": "Analysis results",
"orderAnalysisPackage": "Telli analüüside pakett",
"orderAnalysisPackage": "Order analysis package",
"orderAnalysis": "Order analysis",
"orderHealthAnalysis": "Telli terviseuuring",
"orderHealthAnalysis": "Order health check",
"account": "Account",
"members": "Members",
"billing": "Billing",
"dashboard": "Dashboard",
"settings": "Settings",
"profile": "Profile",
"application": "Application"
"application": "Application",
"pickTime": "Pick time",
"preferences": "Preferences",
"security": "Security"
},
"roles": {
"owner": {
@@ -91,31 +94,53 @@
}
},
"otp": {
"requestVerificationCode": "Request Verification Code",
"requestVerificationCodeDescription": "We must verify your identity to continue with this action. We'll send a verification code to the email address {{email}}.",
"sendingCode": "Sending Code...",
"sendVerificationCode": "Send Verification Code",
"enterVerificationCode": "Enter Verification Code",
"codeSentToEmail": "We've sent a verification code to the email address {{email}}.",
"verificationCode": "Verification Code",
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email.",
"requestVerificationCode": "Please request a verification code",
"requestVerificationCodeDescription": "We need to verify your identity before continuing. We will send a code to your email address {{email}}.",
"sendingCode": "Sending code...",
"sendVerificationCode": "Send verification code",
"enterVerificationCode": "Enter verification code",
"codeSentToEmail": "We have sent a code to your email address {{email}}.",
"verificationCode": "Verification code",
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email address.",
"verifying": "Verifying...",
"verifyCode": "Verify Code",
"requestNewCode": "Request New Code",
"verifyCode": "Verify code",
"requestNewCode": "Request new code",
"errorSendingCode": "Error sending code. Please try again."
},
"cookieBanner": {
"title": "Hey, we use cookies 🍪",
"description": "This website uses cookies to ensure you get the best experience on our website.",
"description": "This website uses cookies to ensure the best experience.",
"reject": "Reject",
"accept": "Accept"
},
"formField": {
"companyName": "Company name",
"contactPerson": "Contact person",
"email": "Email",
"phone": "Phone",
"firstName": "First name",
"lastName": "Last name",
"personalCode": "Personal code",
"city": "City",
"weight": "Weight",
"height": "Height",
"occurance": "Support frequency",
"amount": "Amount",
"selectDate": "Select date"
},
"wallet": {
"balance": "Your MedReport account balance",
"expiredAt": "Valid until {{expiredAt}}"
},
"doctor": "Doctor",
"save": "Save",
"saveAsDraft": "Save as draft",
"confirm": "Confirm",
"previous": "Previous",
"next": "Next",
"invalidDataError": "Invalid data submitted",
"language": "Language"
"invalidDataError": "Invalid data",
"language": "Language",
"yes": "Yes",
"no": "No",
"preferNotToAnswer": "Prefer not to answer"
}

View File

@@ -1,5 +1,5 @@
{
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
"recentlyCheckedDescription": "Here are your most important health indicators.",
"respondToQuestion": "Respond",
"gender": "Gender",
"male": "Male",

View File

@@ -0,0 +1,7 @@
{
"invalidNumber": "Invalid number",
"invalidEmail": "Invalid email",
"tooShort": "Too short",
"tooLong": "Too long",
"invalidPhone": "Invalid phone"
}

View File

@@ -1,125 +1,129 @@
{
"accountTabLabel": "Account Settings",
"accountTabDescription": "Manage your account settings",
"homePage": "Home",
"billingTab": "Billing",
"settingsTab": "Settings",
"multiFactorAuth": "Multi-Factor Authentication",
"multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account",
"updateProfileSuccess": "Profile successfully updated",
"updateProfileError": "Encountered an error. Please try again",
"updatePasswordSuccess": "Password update request successful",
"updatePasswordSuccessMessage": "Your password has been successfully updated!",
"updatePasswordError": "Encountered an error. Please try again",
"updatePasswordLoading": "Updating password...",
"updateProfileLoading": "Updating profile...",
"name": "Your Name",
"nameDescription": "Update your name to be displayed on your profile",
"emailLabel": "Email Address",
"accountImage": "Your Profile Picture",
"accountImageDescription": "Please choose a photo to upload as your profile picture.",
"profilePictureHeading": "Upload a Profile Picture",
"profilePictureSubheading": "Choose a photo to upload as your profile picture.",
"updateProfileSubmitLabel": "Update Profile",
"updatePasswordCardTitle": "Update your Password",
"updatePasswordCardDescription": "Update your password to keep your account secure.",
"currentPassword": "Current Password",
"newPassword": "New Password",
"repeatPassword": "Repeat New Password",
"repeatPasswordDescription": "Please repeat your new password to confirm it",
"yourPassword": "Your Password",
"updatePasswordSubmitLabel": "Update Password",
"updateEmailCardTitle": "Update your Email",
"updateEmailCardDescription": "Update your email address you use to login to your account",
"newEmail": "Your New Email",
"repeatEmail": "Repeat Email",
"updateEmailSubmitLabel": "Update Email Address",
"updateEmailSuccess": "Email update request successful",
"updateEmailSuccessMessage": "We sent you an email to confirm your new email address. Please check your inbox and click on the link to confirm your new email address.",
"updateEmailLoading": "Updating your email...",
"updateEmailError": "Email not updated. Please try again",
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
"emailNotMatching": "Emails do not match. Make sure you're using the correct email",
"passwordNotChanged": "Your password has not changed",
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
"cannotUpdatePassword": "You cannot update your password because your account is not linked to any.",
"setupMfaButtonLabel": "Setup a new Factor",
"multiFactorSetupErrorHeading": "Setup Failed",
"multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.",
"multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication",
"multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.",
"factorNameLabel": "A memorable name to identify this factor",
"factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14",
"factorNameSubmitLabel": "Set factor name",
"unenrollTooltip": "Unenroll this factor",
"unenrollingFactor": "Unenrolling factor...",
"unenrollFactorSuccess": "Factor successfully unenrolled",
"unenrollFactorError": "Unenrolling factor failed",
"factorsListError": "Error loading factors list",
"factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.",
"factorName": "Factor Name",
"factorType": "Type",
"factorStatus": "Status",
"mfaEnabledSuccessTitle": "Multi-Factor authentication is enabled",
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
"verificationCode": "Verification Code",
"addEmailAddress": "Add Email address",
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above",
"loadingFactors": "Loading factors...",
"enableMfaFactor": "Enable Factor",
"disableMfaFactor": "Disable Factor",
"qrCodeErrorHeading": "QR Code Error",
"qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code",
"multiFactorSetupSuccess": "Factor successfully enrolled",
"submitVerificationCode": "Submit Verification Code",
"mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled",
"verifyingCode": "Verifying code...",
"invalidVerificationCodeHeading": "Invalid Verification Code",
"invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.",
"unenrollFactorModalHeading": "Unenroll Factor",
"unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
"unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
"unenrollFactorModalButtonLabel": "Yes, unenroll factor",
"selectFactor": "Choose a factor to verify your identity",
"disableMfa": "Disable Multi-Factor Authentication",
"disableMfaButtonLabel": "Disable MFA",
"confirmDisableMfaButtonLabel": "Yes, disable MFA",
"disablingMfa": "Disabling Multi-Factor Authentication. Please wait...",
"disableMfaSuccess": "Multi-Factor Authentication successfully disabled",
"disableMfaError": "Sorry, we encountered an error. MFA has not been disabled.",
"sendingEmailVerificationLink": "Sending Email...",
"sendEmailVerificationLinkSuccess": "Verification link successfully sent",
"sendEmailVerificationLinkError": "Sorry, we weren't able to send you the email",
"sendVerificationLinkSubmitLabel": "Send Verification Link",
"sendVerificationLinkSuccessLabel": "Email sent! Check your Inbox",
"verifyEmailAlertHeading": "Please verify your email to enable MFA",
"verificationLinkAlertDescription": "Your email is not yet verified. Please verify your email to be able to set up Multi-Factor Authentication.",
"authFactorName": "Factor Name (optional)",
"authFactorNameHint": "Assign a name that helps you remember the phone number used",
"loadingUser": "Loading user details. Please wait...",
"linkPhoneNumber": "Link Phone Number",
"dangerZone": "Danger Zone",
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.",
"deleteAccount": "Delete your Account",
"deletingAccount": "Deleting account. Please wait...",
"deleteAccountDescription": "This will delete your account and the accounts you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone.",
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
"deleteAccountErrorHeading": "Sorry, we couldn't delete your account",
"needsReauthentication": "Reauthentication Required",
"needsReauthenticationDescription": "You need to reauthenticate to change your password. Please sign out and sign in again to change your password.",
"language": "Language",
"languageDescription": "Choose your preferred language",
"noTeamsYet": "You don't have any teams yet.",
"createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team",
"accountTabLabel": "Konto seaded",
"accountTabDescription": "Halda oma konto seadeid ja e-posti eelistusi.",
"preferencesTabLabel": "Eelistused",
"preferencesTabDescription": "Halda oma eelistusi.",
"securityTabLabel": "Turvalisus",
"securityTabDescription": "Kaitse oma kontot.",
"homePage": "Avaleht",
"billingTab": "Arveldamine",
"settingsTab": "Seaded",
"multiFactorAuth": "Mitmefaktoriline autentimine",
"multiFactorAuthDescription": "Sea üles mitmefaktoriline autentimine, et oma kontot rohkem turvata",
"updateProfileSuccess": "Profiil edukalt uuendatud",
"updateProfileError": "Ilmnes viga. Palun proovi uuesti",
"updatePasswordSuccess": "Parooli uuendamine õnnestus",
"updatePasswordSuccessMessage": "Sinu parool on edukalt uuendatud!",
"updatePasswordError": "Ilmnes viga. Palun proovi uuesti",
"updatePasswordLoading": "Parooli uuendamine...",
"updateProfileLoading": "Profiili uuendamine...",
"name": "Sinu nimi",
"nameDescription": "Uuenda oma nime, mis kuvatakse profiilil",
"emailLabel": "E-posti aadress",
"accountImage": "Sinu profiilipilt",
"accountImageDescription": "Vali foto, mida soovid profiilipildina üles laadida.",
"profilePictureHeading": "Laadi üles profiilipilt",
"profilePictureSubheading": "Vali foto, mida soovid profiilipildina üles laadida.",
"updateProfileSubmitLabel": "Uuenda profiili",
"updatePasswordCardTitle": "Uuenda oma parool",
"updatePasswordCardDescription": "Uuenda oma parooli, et hoida oma konto turvaline.",
"currentPassword": "Praegune parool",
"newPassword": "Uus parool",
"repeatPassword": "Korda uut parooli",
"repeatPasswordDescription": "Palun korda oma uus parool, et seda kinnitada",
"yourPassword": "Sinu parool",
"updatePasswordSubmitLabel": "Uuenda parooli",
"updateEmailCardTitle": "Uuenda oma e-posti",
"updateEmailCardDescription": "Uuenda e-posti aadressi, mida kasutad kontole sisselogimiseks",
"newEmail": "Sinu uus e-post",
"repeatEmail": "Korda e-posti",
"updateEmailSubmitLabel": "Uuenda e-posti aadressi",
"updateEmailSuccess": "E-posti uuendamine õnnestus",
"updateEmailSuccessMessage": "Saadame sulle kinnituskirja uue e-posti aadressi kinnitamiseks. Palun vaata oma postkasti ja klõpsa lingil.",
"updateEmailLoading": "E-posti uuendamine...",
"updateEmailError": "E-posti ei uuendatud. Palun proovi uuesti",
"passwordNotMatching": "Paroolid ei ühti. Veendu, et kasutad õiget parooli",
"emailNotMatching": "E-postid ei ühti. Veendu, et kasutad õiget e-posti",
"passwordNotChanged": "Sinu parool ei ole muutunud",
"emailsNotMatching": "E-postid ei ühti. Veendu, et kasutad õiget e-posti",
"cannotUpdatePassword": "Sa ei saa oma parooli uuendada, kuna sinu kontot ei ole lingitud ühegi parooliga.",
"setupMfaButtonLabel": "Sea uus faktor",
"multiFactorSetupErrorHeading": "Seadistamine ebaõnnestus",
"multiFactorSetupErrorDescription": "Vabandame, tekkis viga faktori seadistamisel. Palun proovi uuesti.",
"multiFactorAuthHeading": "Turvake oma konto mitmefaktorilise autentimisega",
"multiFactorModalHeading": "Kasuta oma autentimisrakendust QR-koodi skannimiseks. Seejärel sisesta genereeritud kood.",
"factorNameLabel": "Meeldejääv nimi faktori tuvastamiseks",
"factorNameHint": "Kasuta lihtsat nime, et hiljem seda faktorit kergesti tuvastada. Nt iPhone 14",
"factorNameSubmitLabel": "Määra faktori nimi",
"unenrollTooltip": "Tühista selle faktori registreerimine",
"unenrollingFactor": "Faktori registreerimine tühistatakse...",
"unenrollFactorSuccess": "Faktor edukalt tühistatud",
"unenrollFactorError": "Faktori tühistamine ebaõnnestus",
"factorsListError": "Faktorite nimekirja laadimisel tekkis viga",
"factorsListErrorDescription": "Vabandame, ei õnnestunud faktorite nimekirja laadida. Palun proovi uuesti.",
"factorName": "Faktori nimi",
"factorType": "Tüüp",
"factorStatus": "Staatus",
"mfaEnabledSuccessTitle": "Mitmefaktoriline autentimine on aktiveeritud",
"mfaEnabledSuccessDescription": "Palju õnne! Sa oled edukalt registreeritud mitmefaktorilise autentimise protsessi. Nüüd pääsed oma kontole parooli ja autentimiskoodi abil.",
"verificationCode": "Kinnituskood",
"addEmailAddress": "Lisa e-posti aadress",
"verifyActivationCodeDescription": "Sisesta 6-kohaline kood, mille sinu autentimisrakendus genereeris",
"loadingFactors": "Faktorite laadimine...",
"enableMfaFactor": "Luba faktor",
"disableMfaFactor": "Keela faktor",
"qrCodeErrorHeading": "QR-koodi viga",
"qrCodeErrorDescription": "Vabandame, QR-koodi genereerimine ebaõnnestus",
"multiFactorSetupSuccess": "Faktor edukalt registreeritud",
"submitVerificationCode": "Esita kinnituskood",
"mfaEnabledSuccessAlert": "Mitmefaktoriline autentimine on aktiveeritud",
"verifyingCode": "Koodi kontrollimine...",
"invalidVerificationCodeHeading": "Vale kinnituskood",
"invalidVerificationCodeDescription": "Sisestatud kinnituskood ei kehti. Palun proovi uuesti.",
"unenrollFactorModalHeading": "Tühista faktori registreerimine",
"unenrollFactorModalDescription": "Sa oled tühistamas selle faktori registreerimist. Sa ei saa seda enam kontole sisselogimiseks kasutada.",
"unenrollFactorModalBody": "Sa oled tühistamas selle faktori registreerimist. Sa ei saa seda enam kontole sisselogimiseks kasutada.",
"unenrollFactorModalButtonLabel": "Jah, tühista faktor",
"selectFactor": "Vali faktor, et tuvastada oma identiteet",
"disableMfa": "Keela mitmefaktoriline autentimine",
"disableMfaButtonLabel": "Keela MFA",
"confirmDisableMfaButtonLabel": "Jah, keela MFA",
"disablingMfa": "Mitmefaktoriline autentimine keelatakse. Palun oota...",
"disableMfaSuccess": "Mitmefaktoriline autentimine edukalt keelatud",
"disableMfaError": "Vabandame, tekkis viga. MFA ei ole keelatud.",
"sendingEmailVerificationLink": "E-kirja saatmine...",
"sendEmailVerificationLinkSuccess": "Kinnituse link edukalt saadetud",
"sendEmailVerificationLinkError": "Vabandame, e-kirja saatmine ebaõnnestus",
"sendVerificationLinkSubmitLabel": "Saada kinnituse link",
"sendVerificationLinkSuccessLabel": "E-post saadetud! Vaata oma postkasti",
"verifyEmailAlertHeading": "Palun kinnita oma e-post, et lubada MFA",
"verificationLinkAlertDescription": "Sinu e-post ei ole veel kinnitatud. Palun kinnita e-post, et saaksid mitmefaktorilise autentimise seadistada.",
"authFactorName": "Faktori nimi (valikuline)",
"authFactorNameHint": "Määra nimi, mis aitab meenutada kasutatud telefoninumbrit",
"loadingUser": "Kasutaja andmete laadimine. Palun oota...",
"linkPhoneNumber": "Seosta telefoninumber",
"dangerZone": "Ohtlik tsoon",
"dangerZoneDescription": "Mõnda toimingut ei saa tagasi võtta. Ole ettevaatlik.",
"deleteAccount": "Kustuta oma konto",
"deletingAccount": "Konto kustutamine. Palun oota...",
"deleteAccountDescription": "See kustutab sinu konto ja kõik kontod, mille omanik sa oled. Samuti tühistatakse kohe kõik aktiivsed tellimused. Seda toimingut ei saa tagasi võtta.",
"deleteProfileConfirmationInputLabel": "Sisesta KUSTUTA, et kinnitada",
"deleteAccountErrorHeading": "Vabandame, me ei saanud sinu kontot kustutada",
"needsReauthentication": "Taastõendamine vajalik",
"needsReauthenticationDescription": "Sa pead uuesti autentima, et muuta oma parooli. Palun logi välja ja seejärel sisse, et parooli muuta.",
"language": "Keel",
"languageDescription": "Vali eelistatud keel",
"noTeamsYet": "Sul ei ole veel meeskondi.",
"createTeam": "Loo meeskond alustamiseks.",
"createTeamButtonLabel": "Loo meeskond",
"createCompanyAccount": "Loo ettevõtte konto",
"requestCompanyAccount": {
"title": "Ettevõtte andmed",
"description": "Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport kasutada kavatsed.",
"description": "Pakkumise saamiseks palun sisesta ettevõtte andmed, millega MedReporti kasutada kavatsed.",
"button": "Küsi pakkumist",
"successTitle": "Päring edukalt saadetud!",
"successDescription": "Saadame teile esimesel võimalusel vastuse",
"successButton": "Tagasi kodulehele"
"successDescription": "Vastame sulle esimesel võimalusel",
"successButton": "Tagasi avalehele"
},
"updateAccount": {
"title": "Isikuandmed",
@@ -129,7 +133,7 @@
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid"
},
"consentModal": {
"title": "Enne toimetama hakkamist",
"title": "Enne alustamist",
"description": "Kas annad nõusoleku, et sinu terviseandmeid kasutatakse anonüümselt tööandja statistikas? Andmed jäävad isikustamata ja aitavad ettevõttel töötajate tervist paremini toetada.",
"reject": "Ei anna nõusolekut",
"accept": "Annan nõusoleku"
@@ -152,5 +156,14 @@
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse...",
"doctorAnalysisSummary": "Arsti kokkuvõte analüüsitulemuste kohta"
"doctorAnalysisSummary": "Arsti kokkuvõte analüüsitulemuste kohta",
"myHabits": "Minu terviseharjumused",
"formField": {
"smoking": "Suitsetan"
},
"updateAccountSuccess": "Konto andmed uuendatud",
"updateAccountError": "Konto andmete uuendamine ebaõnnestus",
"updateAccountPreferencesSuccess": "Konto eelistused uuendatud",
"updateAccountPreferencesError": "Konto eelistused uuendamine ebaõnnestus",
"consents": "Nõusolekud"
}

View File

@@ -1,90 +1,90 @@
{
"signUpHeading": "Create an account",
"signUp": "Sign Up",
"signUpSubheading": "Fill the form below to create an account.",
"signInHeading": "Sign in to your account",
"signInSubheading": "Welcome back! Please enter your details",
"signIn": "Sign In",
"getStarted": "Get started",
"updatePassword": "Update Password",
"signOut": "Sign out",
"signingIn": "Signing in...",
"signingUp": "Signing up...",
"doNotHaveAccountYet": "Do not have an account yet?",
"alreadyHaveAnAccount": "Already have an account?",
"signUpToAcceptInvite": "Please sign in/up to accept the invite",
"clickToAcceptAs": "Click the button below to accept the invite with as <b>{{email}}</b>",
"acceptInvite": "Accept invite",
"acceptingInvite": "Accepting Invite...",
"acceptInviteSuccess": "Invite successfully accepted",
"acceptInviteError": "Error encountered while accepting invite",
"acceptInviteWithDifferentAccount": "Want to accept the invite with a different account?",
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead",
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead",
"signInWithProvider": "Sign in with {{provider}}",
"signInWithPhoneNumber": "Sign in with Phone Number",
"signInWithEmail": "Sign in with Email",
"signUpWithEmail": "Sign up with Email",
"passwordHint": "Ensure it's at least 8 characters",
"repeatPasswordHint": "Type your password again",
"repeatPassword": "Repeat password",
"passwordForgottenQuestion": "Password forgotten?",
"passwordResetLabel": "Reset Password",
"passwordResetSubheading": "Enter your email address below. You will receive a link to reset your password.",
"passwordResetSuccessMessage": "Check your Inbox! We emailed you a link for resetting your Password.",
"passwordRecoveredQuestion": "Password recovered?",
"passwordLengthError": "Please provide a password with at least 6 characters",
"sendEmailLink": "Send Email Link",
"sendingEmailLink": "Sending Email Link...",
"sendLinkSuccessDescription": "Check your email, we just sent you a link. Follow the link to sign in.",
"sendLinkSuccess": "We sent you a link by email",
"sendLinkSuccessToast": "Link successfully sent",
"getNewLink": "Get a new link",
"verifyCodeHeading": "Verify your account",
"verificationCode": "Verification Code",
"verificationCodeHint": "Enter the code we sent you by SMS",
"verificationCodeSubmitButtonLabel": "Submit Verification Code",
"sendingMfaCode": "Sending Verification Code...",
"verifyingMfaCode": "Verifying code...",
"sendMfaCodeError": "Sorry, we couldn't send you a verification code",
"verifyMfaCodeSuccess": "Code verified! Signing you in...",
"verifyMfaCodeError": "Ops! It looks like the code is not correct",
"reauthenticate": "Reauthenticate",
"reauthenticateDescription": "For security reasons, we need you to re-authenticate",
"errorAlertHeading": "Sorry, we could not authenticate you",
"emailConfirmationAlertHeading": "We sent you a confirmation email.",
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.",
"resendLink": "Resend link",
"resendLinkSuccessDescription": "We sent you a new link to your email! Follow the link to sign in.",
"resendLinkSuccess": "Check your email!",
"authenticationErrorAlertHeading": "Authentication Error",
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.",
"sendEmailCode": "Get code to your Email",
"sendingEmailCode": "Sending code...",
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
"emailPlaceholder": "your@email.com",
"inviteAlertHeading": "You have been invited to join a company",
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"orContinueWith": "Or continue with",
"redirecting": "You're in! Please wait...",
"signUpHeading": "Loo konto",
"signUp": "Loo konto",
"signUpSubheading": "Täida allolev vorm, et luua konto.",
"signInHeading": "Logi oma kontole sisse",
"signInSubheading": "Tere tulemast tagasi! Palun sisesta oma andmed",
"signIn": "Logi sisse",
"getStarted": "Alusta",
"updatePassword": "Uuenda parooli",
"signOut": "Logi välja",
"signingIn": "Sisselogimine...",
"signingUp": "Registreerimine...",
"doNotHaveAccountYet": "Kas sul pole veel kontot?",
"alreadyHaveAnAccount": "Kas sul on juba konto?",
"signUpToAcceptInvite": "Palun logi sisse/registreeru, et kutse vastu võtta",
"clickToAcceptAs": "Klõpsa alloleval nupul, et võtta kutse vastu kui <b>{{email}}</b>",
"acceptInvite": "Võta kutse vastu",
"acceptingInvite": "Kutset vastu võttes...",
"acceptInviteSuccess": "Kutse on edukalt vastu võetud",
"acceptInviteError": "Kutse vastuvõtmisel tekkis viga",
"acceptInviteWithDifferentAccount": "Soovid kutse vastu võtta teise kontoga?",
"alreadyHaveAccountStatement": "Mul on juba konto, ma tahan sisse logida",
"doNotHaveAccountStatement": "Mul pole kontot, ma tahan registreeruda",
"signInWithProvider": "Logi sisse teenusega {{provider}}",
"signInWithPhoneNumber": "Logi sisse telefoninumbriga",
"signInWithEmail": "Logi sisse e-posti aadressiga",
"signUpWithEmail": "Registreeru e-posti aadressiga",
"passwordHint": "Veendu, et see oleks vähemalt 8 tähemärki pikk",
"repeatPasswordHint": "Sisesta oma parool uuesti",
"repeatPassword": "Korda parooli",
"passwordForgottenQuestion": "Unustasid parooli?",
"passwordResetLabel": "Taasta parool",
"passwordResetSubheading": "Sisesta oma e-posti aadress alla. Saadame sulle lingi parooli lähtestamiseks.",
"passwordResetSuccessMessage": "Kontrolli oma postkasti! Saatsime sulle parooli lähtestamise lingi.",
"passwordRecoveredQuestion": "Kas parool on taastatud?",
"passwordLengthError": "Palun sisesta parool, mis on vähemalt 6 tähemärki pikk",
"sendEmailLink": "Saada e-posti link",
"sendingEmailLink": "E-posti lingi saatmine...",
"sendLinkSuccessDescription": "Kontrolli oma e-posti, just saatsime sulle lingi. Järgi linki, et sisse logida.",
"sendLinkSuccess": "Saadame sulle lingi e-posti teel",
"sendLinkSuccessToast": "Link edukalt saadetud",
"getNewLink": "Hangi uus link",
"verifyCodeHeading": "Kinnita oma konto",
"verificationCode": "Kinnituskood",
"verificationCodeHint": "Sisesta SMS-iga saadetud kood",
"verificationCodeSubmitButtonLabel": "Sisesta kinnituskood",
"sendingMfaCode": "Kinnituskoodi saatmine...",
"verifyingMfaCode": "Koodi kontrollimine...",
"sendMfaCodeError": "Vabandust, meil ei õnnestunud saata kinnituskoodi",
"verifyMfaCodeSuccess": "Kood kinnitatud! Sisselogimine...",
"verifyMfaCodeError": "Ups! Paistab, et kood ei ole õige",
"reauthenticate": "Autentige uuesti",
"reauthenticateDescription": "Turvalisuse huvides peame teid uuesti autentima",
"errorAlertHeading": "Vabandust, me ei saanud sind autentida",
"emailConfirmationAlertHeading": "Saatsime sulle kinnituskirja e-posti teel.",
"emailConfirmationAlertBody": "Tere tulemast! Palun kontrolli oma e-posti ja klõpsa lingil, et konto kinnitada.",
"resendLink": "Saada link uuesti",
"resendLinkSuccessDescription": "Saime sulle saata uue lingi! Järgi linki, et sisse logida.",
"resendLinkSuccess": "Kontrolli oma e-posti!",
"authenticationErrorAlertHeading": "Autentimise viga",
"authenticationErrorAlertBody": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
"sendEmailCode": "Hangi kood e-posti teel",
"sendingEmailCode": "Koodi saatmine...",
"resetPasswordError": "Vabandust, me ei saanud parooli lähtestada. Palun proovi uuesti.",
"emailPlaceholder": "sinu@email.com",
"inviteAlertHeading": "Sind on kutsutud ettevõttega liituma",
"inviteAlertBody": "Palun logi sisse või registreeru, et kutse vastu võtta ja ettevõttega liituda.",
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
"termsOfService": "Kasutustingimused",
"privacyPolicy": "Privaatsuspoliitika",
"orContinueWith": "Või jätka koos",
"redirecting": "Oled sees! Palun oota...",
"errors": {
"Invalid login credentials": "The credentials entered are invalid",
"User already registered": "This credential is already in use. Please try with another one.",
"Email not confirmed": "Please confirm your email address before signing in",
"default": "We have encountered an error. Please ensure you have a working internet connection and try again",
"generic": "Sorry, we weren't able to authenticate you. Please try again.",
"link": "Sorry, we encountered an error while sending your link. Please try again.",
"codeVerifierMismatch": "It looks like you're trying to sign in using a different browser than the one you used to request the sign in link. Please try again using the same browser.",
"minPasswordLength": "Password must be at least 8 characters long",
"passwordsDoNotMatch": "The passwords do not match",
"minPasswordNumbers": "Password must contain at least one number",
"minPasswordSpecialChars": "Password must contain at least one special character",
"uppercasePassword": "Password must contain at least one uppercase letter",
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action",
"otp_expired": "The email link has expired. Please try again.",
"same_password": "The password cannot be the same as the current password"
"Invalid login credentials": "Sisestatud andmed on valed",
"User already registered": "See konto on juba kasutusel. Palun proovi teisega.",
"Email not confirmed": "Palun kinnita oma e-posti aadress enne sisselogimist",
"default": "Tekkis viga. Palun veendu, et sul on töötav internetiühendus, ja proovi uuesti",
"generic": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
"link": "Vabandust, lingi saatmisel tekkis viga. Palun proovi uuesti.",
"codeVerifierMismatch": "Paistab, et proovid sisse logida teises brauseris kui see, millest lingi taotlesid. Palun proovi uuesti sama brauseriga.",
"minPasswordLength": "Parool peab olema vähemalt 8 tähemärki pikk",
"passwordsDoNotMatch": "Paroolid ei kattu",
"minPasswordNumbers": "Parool peab sisaldama vähemalt ühte numbrit",
"minPasswordSpecialChars": "Parool peab sisaldama vähemalt ühte erimärki",
"uppercasePassword": "Parool peab sisaldama vähemalt ühte suurtähte",
"insufficient_aal": "Palun logi sisse oma mitmeastmelise autentimisega, et seda toimingut teha",
"otp_expired": "E-posti link on aegunud. Palun proovi uuesti.",
"same_password": "Parool ei tohi olla sama, mis praegune parool"
}
}

View File

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

View File

@@ -1,61 +1,61 @@
{
"homeTabLabel": "Home",
"homeTabDescription": "Welcome to your home page",
"accountMembers": "Company Members",
"membersTabDescription": "Here you can manage the members of your company.",
"billingTabLabel": "Billing",
"billingTabDescription": "Manage your billing and subscription",
"dashboardTabLabel": "Dashboard",
"settingsTabLabel": "Settings",
"profileSettingsTabLabel": "Profile",
"subscriptionSettingsTabLabel": "Subscription",
"dashboardTabDescription": "An overview of your account's activity and performance across all your projects.",
"settingsTabDescription": "Manage your settings and preferences.",
"emailAddress": "Email Address",
"password": "Password",
"modalConfirmationQuestion": "Are you sure you want to continue?",
"imageInputLabel": "Click here to upload an image",
"cancel": "Cancel",
"clear": "Clear",
"homeTabLabel": "Avaleht",
"homeTabDescription": "Tere tulemast sinu avalehele",
"accountMembers": "Ettevõtte liikmed",
"membersTabDescription": "Siit saad hallata oma ettevõtte liikmeid.",
"billingTabLabel": "Arveldamine",
"billingTabDescription": "Halda oma arveldamist ja tellimusi",
"dashboardTabLabel": "Ülevaade",
"settingsTabLabel": "Seaded",
"profileSettingsTabLabel": "Profiil",
"subscriptionSettingsTabLabel": "Tellimus",
"dashboardTabDescription": "Ülevaade sinu konto tegevusest ja tulemuste kohta kõigis projektides.",
"settingsTabDescription": "Halda oma seadeid ja eelistusi.",
"emailAddress": "E-posti aadress",
"password": "Parool",
"modalConfirmationQuestion": "Oled sa kindel, et soovid jätkata?",
"imageInputLabel": "Klikka siia, et üles laadida pilt",
"cancel": "Tühista",
"clear": "Kustuta",
"close": "Sulge",
"notFound": "Not Found",
"backToHomePage": "Back to Home Page",
"notFound": "Ei leitud",
"backToHomePage": "Tagasi avalehele",
"goBack": "Tagasi",
"genericServerError": "Sorry, something went wrong.",
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
"pageNotFound": "Sorry, this page does not exist.",
"pageNotFoundSubHeading": "Apologies, the page you were looking for was not found",
"genericError": "Sorry, something went wrong.",
"genericErrorSubHeading": "Apologies, an error occurred while processing your request. Please contact us if the issue persists.",
"anonymousUser": "Anonymous",
"tryAgain": "Try Again",
"theme": "Theme",
"lightTheme": "Light",
"darkTheme": "Dark",
"systemTheme": "System",
"expandSidebar": "Expand Sidebar",
"collapseSidebar": "Collapse Sidebar",
"documentation": "Documentation",
"genericServerError": "Vabandame, midagi läks valesti.",
"genericServerErrorHeading": "Vabandame, midagi läks valesti teie päringu töötlemisel. Palun võtke meiega ühendust, kui probleem püsib.",
"pageNotFound": "Vabandame, seda lehte ei eksisteeri.",
"pageNotFoundSubHeading": "Vabandame, lehte, mida otsisite, ei leitud",
"genericError": "Vabandame, midagi läks valesti.",
"genericErrorSubHeading": "Vabandame, ilmnes viga teie päringu töötlemisel. Palun võtke meiega ühendust, kui probleem püsib.",
"anonymousUser": "Anonüümne",
"tryAgain": "Proovi uuesti",
"theme": "Teema",
"lightTheme": "Hele",
"darkTheme": "Tume",
"systemTheme": "Süsteem",
"expandSidebar": "Laienda külgriba",
"collapseSidebar": "Kokkuvoldi külgriba",
"documentation": "Dokumentatsioon",
"getStarted": "Alusta!",
"getStartedWithPlan": "Get Started with {{plan}}",
"retry": "Retry",
"contactUs": "Contact Us",
"loading": "Loading. Please wait...",
"yourAccounts": "Your Accounts",
"continue": "Continue",
"skip": "Skip",
"signedInAs": "Signed in as",
"getStartedWithPlan": "Alusta plaaniga {{plan}}",
"retry": "Proovi uuesti",
"contactUs": "Võta meiega ühendust",
"loading": "Laadimine. Palun oota...",
"yourAccounts": "Sinu kontod",
"continue": "Jätka",
"skip": "Jäta vahele",
"signedInAs": "Sisselogitud kasutajana",
"pageOfPages": "Leht {{page}} / {{total}}",
"noData": "Andmed puuduvad",
"pageNotFoundHeading": "Ouch! :|",
"errorPageHeading": "Ouch! :|",
"notifications": "Notifications",
"noNotifications": "No notifications",
"justNow": "Just now",
"newVersionAvailable": "New version available",
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
"newVersionSubmitButton": "Reload and Update",
"back": "Back",
"noData": "Andmeid puudub",
"pageNotFoundHeading": "Ups! :|",
"errorPageHeading": "Ups! :|",
"notifications": "Teavitused",
"noNotifications": "Teavitusi pole",
"justNow": "Just nüüd",
"newVersionAvailable": "Uus versioon saadaval",
"newVersionAvailableDescription": "Rakenduse uus versioon on saadaval. Soovitame lehe värskendada, et saada uusimad uuendused ja vältida probleeme.",
"newVersionSubmitButton": "Värskenda ja uuenda",
"back": "Tagasi",
"welcome": "Tere tulemast",
"shoppingCart": "Ostukorv",
"shoppingCartCount": "Ostukorv ({{count}})",
@@ -63,10 +63,10 @@
"myActions": "Minu toimingud",
"healthPackageComparison": {
"label": "Tervisepakettide võrdlus",
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeks) põhjal tehtud personaalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
},
"routes": {
"home": "Home",
"home": "Avaleht",
"overview": "Ülevaade",
"booking": "Broneeri aeg",
"myOrders": "Minu tellimused",
@@ -74,47 +74,49 @@
"orderAnalysisPackage": "Telli analüüside pakett",
"orderAnalysis": "Telli analüüs",
"orderHealthAnalysis": "Telli terviseuuring",
"account": "Account",
"members": "Members",
"billing": "Billing",
"account": "Konto",
"members": "Liikmed",
"billing": "Arveldamine",
"dashboard": "Ülevaade",
"settings": "Settings",
"profile": "Profile",
"application": "Application",
"pickTime": "Vali aeg"
"settings": "Seaded",
"profile": "Profiil",
"application": "Rakendus",
"pickTime": "Vali aeg",
"preferences": "Eelistused",
"security": "Turvalisus"
},
"roles": {
"owner": {
"label": "Admin"
},
"member": {
"label": "Member"
"label": "Liige"
}
},
"otp": {
"requestVerificationCode": "Request Verification Code",
"requestVerificationCodeDescription": "We must verify your identity to continue with this action. We'll send a verification code to the email address {{email}}.",
"sendingCode": "Sending Code...",
"sendVerificationCode": "Send Verification Code",
"enterVerificationCode": "Enter Verification Code",
"codeSentToEmail": "We've sent a verification code to the email address {{email}}.",
"verificationCode": "Verification Code",
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email.",
"verifying": "Verifying...",
"verifyCode": "Verify Code",
"requestNewCode": "Request New Code",
"errorSendingCode": "Error sending code. Please try again."
"requestVerificationCode": "Palun taotle kinnituskood",
"requestVerificationCodeDescription": "Peame sinu identiteedi kontrollima, et jätkata. Saadame koodi e-posti aadressile {{email}}.",
"sendingCode": "Koodi saatmine...",
"sendVerificationCode": "Saada kinnituskood",
"enterVerificationCode": "Sisesta kinnituskood",
"codeSentToEmail": "Oleme saatnud koodi e-posti aadressile {{email}}.",
"verificationCode": "Kinnituskood",
"enterCodeFromEmail": "Sisesta 6-kohaline kood, mille saatsime sinu e-posti aadressile.",
"verifying": "Kontrollimine...",
"verifyCode": "Kontrolli koodi",
"requestNewCode": "Taotle uut koodi",
"errorSendingCode": "Koodi saatmisel tekkis viga. Proovi uuesti."
},
"cookieBanner": {
"title": "Hey, we use cookies 🍪",
"description": "This website uses cookies to ensure you get the best experience on our website.",
"reject": "Reject",
"accept": "Accept"
"title": "Hei, me kasutame küpsiseid 🍪",
"description": "See veebileht kasutab küpsiseid, et tagada parim kasutuskogemus.",
"reject": "Keela",
"accept": "Luba"
},
"formField": {
"companyName": "Ettevõtte nimi",
"contactPerson": "Kontaktisik",
"email": "E-mail",
"email": "E-post",
"phone": "Telefon",
"firstName": "Eesnimi",
"lastName": "Perenimi",
@@ -127,7 +129,7 @@
"selectDate": "Vali kuupäev"
},
"wallet": {
"balance": "Sinu MedReporti konto seis",
"balance": "Sinu MedReporti konto saldo",
"expiredAt": "Kehtiv kuni {{expiredAt}}"
},
"doctor": "Arst",
@@ -137,5 +139,8 @@
"previous": "Eelmine",
"next": "Järgmine",
"invalidDataError": "Vigased andmed",
"language": "Keel"
}
"language": "Keel",
"yes": "Jah",
"no": "Ei",
"preferNotToAnswer": "Eelistan mitte vastata"
}

View File

@@ -1,5 +1,5 @@
{
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
"recentlyCheckedDescription": "Siin on sinu olulisemad tervisenäitajad",
"respondToQuestion": "Vasta küsimusele",
"gender": "Sugu",
"male": "Mees",

View File

@@ -0,0 +1,7 @@
{
"invalidNumber": "Vigane arv",
"invalidEmail": "Vigane email",
"tooShort": "Liiga lühike sisend",
"tooLong": "Liiga pikk sisend",
"invalidPhone": "Vigane telefoninumber"
}

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