Merge pull request #73 from MR-medreport/develop

develop -> main
This commit is contained in:
2025-09-04 10:42:33 +00:00
committed by GitHub
93 changed files with 4598 additions and 3969 deletions

View File

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

View File

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

View File

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

View File

@@ -3,17 +3,17 @@ import { z } from 'zod';
export const UpdateAccountSchema = z.object({ export const UpdateAccountSchema = z.object({
firstName: z firstName: z
.string({ .string({
required_error: 'First name is required', error: 'First name is required',
}) })
.nonempty(), .nonempty(),
lastName: z lastName: z
.string({ .string({
required_error: 'Last name is required', error: 'Last name is required',
}) })
.nonempty(), .nonempty(),
personalCode: z personalCode: z
.string({ .string({
required_error: 'Personal code is required', error: 'Personal code is required',
}) })
.nonempty(), .nonempty(),
email: z.string().email({ email: z.string().email({
@@ -21,21 +21,25 @@ export const UpdateAccountSchema = z.object({
}), }),
phone: z phone: z
.string({ .string({
required_error: 'Phone number is required', error: 'Phone number is required',
}) })
.nonempty(), .nonempty(),
city: z.string().optional(), city: z.string().optional(),
weight: z weight: z
.number({ .number({
required_error: 'Weight is required', error: (issue) =>
invalid_type_error: 'Weight must be a number', issue.input === undefined
? 'Weight is required'
: 'Weight must be a number',
}) })
.gt(0, { message: 'Weight must be greater than 0' }), .gt(0, { message: 'Weight must be greater than 0' }),
height: z height: z
.number({ .number({
required_error: 'Height is required', error: (issue) =>
invalid_type_error: 'Height must be a number', issue.input === undefined
? 'Height is required'
: 'Height must be a number',
}) })
.gt(0, { message: 'Height must be greater than 0' }), .gt(0, { message: 'Height must be greater than 0' }),
userConsent: z.boolean().refine((val) => val === true, { 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 { placeOrder, retrieveCart } from "@lib/data/cart";
import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
import { createOrder } from '~/lib/services/order.service'; 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 { createNotificationsApi } from '@kit/notifications/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { AccountWithParams } from '@kit/accounts/api'; import { AccountWithParams } from '@kit/accounts/api';
@@ -20,12 +20,12 @@ const env = () => z
.object({ .object({
emailSender: z emailSender: z
.string({ .string({
required_error: 'EMAIL_SENDER is required', error: 'EMAIL_SENDER is required',
}) })
.min(1), .min(1),
siteUrl: z siteUrl: z
.string({ .string({
required_error: 'NEXT_PUBLIC_SITE_URL is required', error: 'NEXT_PUBLIC_SITE_URL is required',
}) })
.min(1), .min(1),
}) })
@@ -114,7 +114,7 @@ export async function processMontonioCallback(orderToken: string) {
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false }); const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder }); const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements }); const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
const { productTypes } = await listProductTypes(); const { productTypes } = await listProductTypes();

View File

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

@@ -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 { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import { import {
Activity, Activity,
ChevronRight,
Clock9, Clock9,
Droplets,
Pill, Pill,
Scale, Scale,
TrendingUp, TrendingUp,
User, User,
} from 'lucide-react'; } 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 { getPersonParameters } from '@kit/shared/utils';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { import {
Card, Card,
CardContent,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardProps,
} from '@kit/ui/card'; } from '@kit/ui/card';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { isNil } from 'lodash';
import { BmiCategory } from '~/lib/types/bmi'; import { BmiCategory } from '~/lib/types/bmi';
import { import {
bmiFromMetric, bmiFromMetric,
getBmiBackgroundColor, getBmiBackgroundColor,
getBmiStatus, getBmiStatus,
} from '~/lib/utils'; } 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 = ({ const cards = ({
gender, gender,
@@ -41,111 +49,91 @@ const cards = ({
height, height,
weight, weight,
bmiStatus, bmiStatus,
smoking,
}: { }: {
gender?: string; gender?: string;
age?: number; age?: number;
height?: number | null; height?: number | null;
weight?: number | null; weight?: number | null;
bmiStatus: BmiCategory | null; bmiStatus: BmiCategory | null;
smoking?: boolean | null;
}) => [ }) => [
{ {
title: 'dashboard:gender', title: 'dashboard:gender',
description: gender ?? 'dashboard:male', description: gender ?? 'dashboard:male',
icon: <User />, icon: <User />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:age', title: 'dashboard:age',
description: age ? `${age}` : '-', description: age ? `${age}` : '-',
icon: <Clock9 />, icon: <Clock9 />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:height', title: 'dashboard:height',
description: height ? `${height}cm` : '-', description: height ? `${height}cm` : '-',
icon: <RulerHorizontalIcon className="size-4" />, icon: <RulerHorizontalIcon className="size-4" />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:weight', title: 'dashboard:weight',
description: weight ? `${weight}kg` : '-', description: weight ? `${weight}kg` : '-',
icon: <Scale />, icon: <Scale />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:bmi', title: 'dashboard:bmi',
description: bmiFromMetric(weight || 0, height || 0).toString(), description: bmiFromMetric(weight || 0, height || 0).toString(),
icon: <TrendingUp />, icon: <TrendingUp />,
iconBg: getBmiBackgroundColor(bmiStatus), iconBg: getBmiBackgroundColor(bmiStatus),
}, },
{ {
title: 'dashboard:bloodPressure', title: 'dashboard:bloodPressure',
description: '-', description: '-',
icon: <Activity />, icon: <Activity />,
iconBg: 'bg-warning', iconBg: 'bg-warning',
}, },
{ {
title: 'dashboard:cholesterol', title: 'dashboard:cholesterol',
description: '-', description: '-',
icon: <BlendingModeIcon className="size-4" />, icon: <BlendingModeIcon className="size-4" />,
iconBg: 'bg-destructive', iconBg: 'bg-destructive',
}, },
{ {
title: 'dashboard:ldlCholesterol', title: 'dashboard:ldlCholesterol',
description: '-', description: '-',
icon: <Pill />, icon: <Pill />,
iconBg: 'bg-warning', iconBg: 'bg-warning',
}, },
// { // {
// title: 'Score 2', // title: 'Score 2',
// description: 'Normis', // description: 'Normis',
// icon: <LineChart />, // icon: <LineChart />,
// iconBg: 'bg-success', // iconBg: 'bg-success',
// }, // },
// { {
// title: 'dashboard:smoking', title: 'dashboard:smoking',
// description: 'dashboard:respondToQuestion', description:
// descriptionColor: 'text-primary', isNil(smoking)
// icon: ( ? 'dashboard:respondToQuestion'
// <Button size="icon" variant="outline" className="px-2 text-black"> : !!smoking
// <ChevronRight className="size-4 stroke-2" /> ? 'common:yes'
// </Button> : 'common:no',
// ), descriptionColor: 'text-primary',
// cardVariant: 'gradient-success' as CardProps['variant'], 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 = [ const IS_SHOWN_RECOMMENDATIONS = false as boolean;
{
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 Dashboard({ export default function Dashboard({
account, account,
@@ -160,8 +148,8 @@ export default function Dashboard({
const params = getPersonParameters(account.personal_code!); const params = getPersonParameters(account.personal_code!);
const bmiStatus = getBmiStatus(bmiThresholds, { const bmiStatus = getBmiStatus(bmiThresholds, {
age: params?.age || 0, age: params?.age || 0,
height: account.account_params?.[0]?.height || 0, height: account.accountParams?.height || 0,
weight: account.account_params?.[0]?.weight || 0, weight: account.accountParams?.weight || 0,
}); });
return ( return (
@@ -170,21 +158,22 @@ export default function Dashboard({
{cards({ {cards({
gender: params?.gender, gender: params?.gender,
age: params?.age, age: params?.age,
height: account.account_params?.[0]?.height, height: account.accountParams?.height,
weight: account.account_params?.[0]?.weight, weight: account.accountParams?.weight,
bmiStatus, bmiStatus,
smoking: account.accountParams?.isSmoker,
}).map( }).map(
({ ({
title, title,
description, description,
icon, icon,
iconBg, iconBg,
// cardVariant, cardVariant,
// descriptionColor, // descriptionColor,
}) => ( }) => (
<Card <Card
key={title} key={title}
// variant={cardVariant} variant={cardVariant}
className="flex flex-col justify-between" className="flex flex-col justify-between"
> >
<CardHeader className="items-end-safe"> <CardHeader className="items-end-safe">
@@ -211,79 +200,7 @@ export default function Dashboard({
), ),
)} )}
</div> </div>
<Card> {IS_SHOWN_RECOMMENDATIONS && <DashboardRecommendations />}
<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>
</> </>
); );
} }

View File

@@ -31,7 +31,8 @@ export async function HomeMenuNavigation(props: {
}) })
: 0; : 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; const hasCartItems = cartQuantityTotal > 0;
return ( return (
@@ -39,15 +40,18 @@ export async function HomeMenuNavigation(props: {
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}> <div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
<AppLogo href={pathsConfig.app.home} /> <AppLogo href={pathsConfig.app.home} />
</div> </div>
<Search {/* TODO: add search functionality */}
{/* <Search
className="flex grow" className="flex grow"
startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />} startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />}
/> /> */}
<div className="flex items-center justify-end gap-3"> <div className="flex items-center justify-end gap-3">
{/* TODO: add wallet functionality
<Card className="px-6 py-2"> <Card className="px-6 py-2">
<span>€ {Number(0).toFixed(2).replace('.', ',')}</span> <span>€ {Number(0).toFixed(2).replace('.', ',')}</span>
</Card> </Card>
*/}
{hasCartItems && ( {hasCartItems && (
<Button <Button
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2" 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> <span className="flex items-center text-nowrap">{totalValue}</span>
</Button> </Button>
)} )}
<Link href="/home/cart"> <Link href={pathsConfig.app.cart}>
<Button <Button
variant="ghost" variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2" 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 Link from 'next/link';
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
import { StoreCart } from '@medusajs/types'; 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 { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { ApplicationRoleEnum } from '@kit/accounts/types/accounts'; import { ApplicationRoleEnum } from '@kit/accounts/types/accounts';
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
import { import {
pathsConfig, pathsConfig,
personalAccountNavigationConfig, personalAccountNavigationConfig,
@@ -91,7 +93,7 @@ export function HomeMobileNavigation(props: {
<If condition={props.cart && hasCartItems}> <If condition={props.cart && hasCartItems}>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownLink <DropdownLink
path="/home/cart" path={pathsConfig.app.cart}
label="common:shoppingCartCount" label="common:shoppingCartCount"
Icon={<ShoppingCart className="stroke-[1.5px]" />} Icon={<ShoppingCart className="stroke-[1.5px]" />}
labelOptions={{ count: cartQuantityTotal }} 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"; "use client";
import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react'; import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { import {
@@ -15,12 +16,14 @@ import { handleAddToCart } from '~/lib/services/medusaCart.service';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip'; import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { toast } from '@kit/ui/sonner'; import { toast } from '@kit/ui/sonner';
import { formatCurrency } from '@/packages/shared/src/utils';
export type OrderAnalysisCard = Pick< export type OrderAnalysisCard = Pick<
StoreProduct, 'title' | 'description' | 'subtitle' StoreProduct, 'title' | 'description' | 'subtitle'
> & { > & {
isAvailable: boolean; isAvailable: boolean;
variant: { id: string }; variant: { id: string };
price: number | null;
}; };
export default function OrderAnalysesCards({ export default function OrderAnalysesCards({
@@ -30,23 +33,26 @@ export default function OrderAnalysesCards({
analyses: OrderAnalysisCard[]; analyses: OrderAnalysisCard[];
countryCode: string; countryCode: string;
}) { }) {
const [isAddingToCart, setIsAddingToCart] = useState(false);
const { i18n: { language } } = useTranslation()
const [variantAddingToCart, setVariantAddingToCart] = useState<string | null>(null);
const handleSelect = async (variantId: string) => { const handleSelect = async (variantId: string) => {
if (isAddingToCart) { if (variantAddingToCart) {
return null; return null;
} }
setIsAddingToCart(true); setVariantAddingToCart(variantId);
try { try {
await handleAddToCart({ await handleAddToCart({
selectedVariant: { id: variantId }, selectedVariant: { id: variantId },
countryCode, countryCode,
}); });
toast.success(<Trans i18nKey={'order-analysis:analysisAddedToCart'} />); toast.success(<Trans i18nKey={'order-analysis:analysisAddedToCart'} />);
setIsAddingToCart(false); setVariantAddingToCart(null);
} catch (e) { } catch (e) {
toast.error(<Trans i18nKey={'order-analysis:analysisAddToCartError'} />); toast.error(<Trans i18nKey={'order-analysis:analysisAddToCartError'} />);
setIsAddingToCart(false); setVariantAddingToCart(null);
console.error(e); console.error(e);
} }
} }
@@ -59,7 +65,15 @@ export default function OrderAnalysesCards({
description, description,
subtitle, subtitle,
isAvailable, isAvailable,
price,
}) => { }) => {
const formattedPrice = typeof price === 'number'
? formatCurrency({
currencyCode: 'eur',
locale: language,
value: price,
})
: null;
return ( return (
<Card <Card
key={title} key={title}
@@ -80,7 +94,7 @@ export default function OrderAnalysesCards({
className="px-2 text-black" className="px-2 text-black"
onClick={() => handleSelect(variant.id)} onClick={() => handleSelect(variant.id)}
> >
{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> </Button>
</div> </div>
)} )}
@@ -91,7 +105,14 @@ export default function OrderAnalysesCards({
{description && ( {description && (
<> <>
{' '} {' '}
<InfoTooltip content={`${description}`} /> <InfoTooltip
content={
<div className='flex flex-col gap-2'>
<span>{formattedPrice}</span>
<span>{description}</span>
</div>
}
/>
</> </>
)} )}
</h5> </h5>

View File

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

View File

@@ -1,11 +1,10 @@
import { cache } from 'react'; import { cache } from 'react';
import { getProductCategories } from '@lib/data/categories'; 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 { listRegions } from '@lib/data/regions';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
import { ServiceCategory } from '../../_components/service-categories';
async function countryCodesLoader() { async function countryCodesLoader() {
const countryCodes = await listRegions().then((regions) => const countryCodes = await listRegions().then((regions) =>
@@ -39,13 +38,20 @@ async function analysesLoader() {
const category = productCategories.find( const category = productCategories.find(
({ metadata }) => metadata?.page === 'order-analysis', ({ metadata }) => metadata?.page === 'order-analysis',
); );
const categoryProducts = category
? await listProducts({
countryCode,
queryParams: { limit: 100, category_id: category.id },
})
: null;
const serviceCategories = productCategories.filter( const serviceCategories = productCategories.filter(
({ parent_category }) => parent_category?.handle === 'tto-categories', ({ parent_category }) => parent_category?.handle === 'tto-categories',
); );
return { return {
analyses: analyses:
category?.products?.map<OrderAnalysisCard>( categoryProducts?.response.products.map<OrderAnalysisCard>(
({ title, description, subtitle, variants, status, metadata }) => { ({ title, description, subtitle, variants, status, metadata }) => {
const variant = variants![0]!; const variant = variants![0]!;
return { return {
@@ -57,6 +63,7 @@ async function analysesLoader() {
}, },
isAvailable: isAvailable:
status === 'published' && !!metadata?.analysisIdOriginal, 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 { use } from 'react';
import { Trans } from '@kit/ui/trans';
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 { 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 function UserSettingsLayout({ children }: React.PropsWithChildren) {
import { HomeLayoutPageHeader } from '../_components/home-page-header'; return <HeaderLayout>{children}</HeaderLayout>;
function UserSettingsLayout(props: React.PropsWithChildren) {
return (
<>
<HomeLayoutPageHeader
title={<Trans i18nKey={'account:routes.settings'} />}
description={<AppBreadcrumbs />}
/>
{props.children}
</>
);
} }
export default withI18n(UserSettingsLayout); 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 { PageBody } from '@kit/ui/page';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
const features = { import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
enableAccountDeletion: featureFlagsConfig.enableAccountDeletion, import AccountSettingsForm from './_components/account-settings-form';
enablePasswordUpdate: authConfig.providers.password, import SettingsSectionHeader from './_components/settings-section-header';
};
const callbackPath = pathsConfig.auth.callback;
const accountHomePath = pathsConfig.app.accountHome;
const paths = {
callback: callbackPath + `?next=${accountHomePath}`,
};
export const generateMetadata = async () => { export const generateMetadata = async () => {
const i18n = await createI18nServerInstance(); const i18n = await createI18nServerInstance();
@@ -33,17 +16,18 @@ export const generateMetadata = async () => {
}; };
}; };
function PersonalAccountSettingsPage() { async function PersonalAccountSettingsPage() {
const user = use(requireUserInServerComponent()); const account = await loadCurrentUserAccount();
return ( return (
<PageBody> <PageBody>
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}> <div className="mx-auto w-full bg-white p-6">
<PersonalAccountSettingsContainer <div className="space-y-6">
userId={user.id} <SettingsSectionHeader
features={features} titleKey="account:accountTabLabel"
paths={paths} descriptionKey="account:accountTabDescription"
/> />
<AccountSettingsForm account={account} />
</div>
</div> </div>
</PageBody> </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'; 'use client';
import Link from 'next/link'; import DropdownLink from '@kit/shared/components/ui/dropdown-link';
import { useRouter } from 'next/navigation'; 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 { AccountSelector } from '@kit/accounts/account-selector';
import { 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: { function TeamAccountsModal(props: {
accounts: Accounts; accounts: Accounts;

View File

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

View File

@@ -1,8 +1,14 @@
import type { Tables } from '@/packages/supabase/src/database.types'; import type { Tables } from '@/packages/supabase/src/database.types';
import { AccountWithParams } from '@kit/accounts/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-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 Account = Tables<{ schema: 'medreport' }, 'accounts'>;
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>; type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>;
@@ -61,7 +67,9 @@ export async function getDoctorAccounts() {
} }
export async function getAssignedDoctorAccount(analysisOrderId: number) { export async function getAssignedDoctorAccount(analysisOrderId: number) {
const { data: doctorUser } = await getSupabaseServerAdminClient() const supabase = getSupabaseServerAdminClient();
const { data: doctorUser } = await supabase
.schema('medreport') .schema('medreport')
.from('doctor_analysis_feedback') .from('doctor_analysis_feedback')
.select('doctor_user_id') .select('doctor_user_id')
@@ -73,7 +81,7 @@ export async function getAssignedDoctorAccount(analysisOrderId: number) {
return null; return null;
} }
const { data } = await getSupabaseServerAdminClient() const { data } = await supabase
.schema('medreport') .schema('medreport')
.from('accounts') .from('accounts')
.select('email') .select('email')
@@ -81,3 +89,58 @@ export async function getAssignedDoctorAccount(analysisOrderId: number) {
return { email: data?.[0]?.email }; 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> { export async function getAnalyses({ ids, originalIds }: { ids?: number[], originalIds?: string[] }): Promise<AnalysesWithGroupsAndElements> {
const { data } = await getSupabaseServerAdminClient() const query = getSupabaseServerAdminClient()
.schema('medreport') .schema('medreport')
.from('analyses') .from('analyses')
.select(`*, analysis_elements(*, analysis_groups(*))`) .select(`*, analysis_elements(*, analysis_groups(*))`);
.in('id', ids); 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; return data as unknown as AnalysesWithGroupsAndElements;
} }

View File

@@ -666,7 +666,7 @@ async function syncPrivateMessage({
unit: element.Mootyhik ?? null, unit: element.Mootyhik ?? null,
original_response_element: element, original_response_element: element,
analysis_name: element.UuringNimi || element.KNimetus, analysis_name: element.UuringNimi || element.KNimetus,
comment: element.UuringuKommentaar comment: element.UuringuKommentaar ?? '',
})), })),
); );
} }
@@ -715,7 +715,7 @@ export async function sendOrderToMedipost({
orderedAnalysisElements, orderedAnalysisElements,
}: { }: {
medusaOrderId: string; medusaOrderId: string;
orderedAnalysisElements: { analysisElementId: number }[]; orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[];
}) { }) {
const medreportOrder = await getOrder({ medusaOrderId }); const medreportOrder = await getOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
@@ -727,8 +727,8 @@ export async function sendOrderToMedipost({
lastName: account.last_name ?? '', lastName: account.last_name ?? '',
phone: account.phone ?? '', phone: account.phone ?? '',
}, },
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId), orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: [], orderedAnalysesIds: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderId: medusaOrderId, orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at), orderCreatedAt: new Date(medreportOrder.created_at),
comment: '', comment: '',
@@ -784,12 +784,13 @@ export async function sendOrderToMedipost({
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
} }
export async function getOrderedAnalysisElementsIds({ export async function getOrderedAnalysisIds({
medusaOrder, medusaOrder,
}: { }: {
medusaOrder: StoreOrder; medusaOrder: StoreOrder;
}): Promise<{ }): Promise<{
analysisElementId: number; analysisElementId?: number;
analysisId?: number;
}[]> { }[]> {
const countryCodes = await listRegions(); const countryCodes = await listRegions();
const countryCode = countryCodes[0]!.countries![0]!.iso_2!; const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
@@ -802,6 +803,14 @@ export async function getOrderedAnalysisElementsIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id })); 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) { async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX)); const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[]; const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
@@ -841,12 +850,13 @@ export async function getOrderedAnalysisElementsIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id })); return analysisElements.map(({ id }) => ({ analysisElementId: id }));
} }
const [analysisPackageElements, orderedAnalysisElements] = await Promise.all([ const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
getOrderedAnalysisPackages(medusaOrder), getOrderedAnalysisPackages(medusaOrder),
getOrderedAnalysisElements(medusaOrder), getOrderedAnalysisElements(medusaOrder),
getOrderedAnalyses(medusaOrder),
]); ]);
return [...analysisPackageElements, ...orderedAnalysisElements]; return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
} }
export async function createMedipostActionLog({ export async function createMedipostActionLog({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,8 @@ import { z } from 'zod';
export const UpdateHealthBenefitSchema = z.object({ export const UpdateHealthBenefitSchema = z.object({
occurance: z occurance: z
.string({ .string({
required_error: 'Occurance is required', error: 'Occurance is required',
}) })
.nonempty(), .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({ .object({
secretKey: z secretKey: z
.string({ .string({
required_error: `Please provide the variable MONTONIO_SECRET_KEY`, error: `Please provide the variable MONTONIO_SECRET_KEY`,
}) })
.min(1), .min(1),
apiUrl: z apiUrl: z
.string({ .string({
required_error: `Please provide the variable MONTONIO_API_URL`, error: `Please provide the variable MONTONIO_API_URL`,
}) })
.min(1), .min(1),
}); });

View File

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

View File

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

View File

@@ -1 +1,3 @@
export * from './user-workspace-context'; 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; personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
return hasAdminRole && hasTotpFactor; return hasAdminRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]); }, [personalAccountData, hasTotpFactor]);
const isDoctor = useMemo(() => { const isDoctor = useMemo(() => {
const hasDoctorRole = const hasDoctorRole =
personalAccountData?.application_role === ApplicationRoleEnum.Doctor; personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
return hasDoctorRole && hasTotpFactor; return hasDoctorRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]); }, [personalAccountData, hasTotpFactor]);
return ( return (
<DropdownMenu> <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 { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors';
import { useSupabase } from '@kit/supabase/hooks/use-supabase'; 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 { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { import {
@@ -46,13 +47,18 @@ import { Trans } from '@kit/ui/trans';
import { MultiFactorAuthSetupDialog } from './multi-factor-auth-setup-dialog'; 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 ( return (
<div className={'flex flex-col space-y-4'}> <div className={'flex flex-col space-y-4'}>
<FactorsTableContainer userId={props.userId} /> <FactorsTableContainer userId={user?.id} />
<div> <div>
<MultiFactorAuthSetupDialog userId={props.userId} /> <MultiFactorAuthSetupDialog userId={user?.id} />
</div> </div>
</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 { Database } from '@kit/supabase/database';
import { import { AnalysisResultDetails, UserAnalysis } from '../types/accounts';
AnalysisResultDetails,
UserAnalysis,
UserAnalysisResponse,
} from '../types/accounts';
export type AccountWithParams = export type AccountWithParams =
Database['medreport']['Tables']['accounts']['Row'] & { Database['medreport']['Tables']['accounts']['Row'] & {
account_params: accountParams:
| Pick< | (Pick<
Database['medreport']['Tables']['account_params']['Row'], Database['medreport']['Tables']['account_params']['Row'],
'weight' | 'height' 'weight' | 'height'
>[] > & {
isSmoker:
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
| null;
})
| null; | null;
}; };
@@ -35,7 +35,9 @@ class AccountsApi {
const { data, error } = await this.client const { data, error } = await this.client
.schema('medreport') .schema('medreport')
.from('accounts') .from('accounts')
.select('*, account_params: account_params (weight, height)') .select(
'*, accountParams: account_params (weight, height, isSmoker:is_smoker)',
)
.eq('id', id) .eq('id', id)
.single(); .single();

View File

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

View File

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

View File

@@ -272,7 +272,12 @@ export async function medusaLoginOrRegister(credentials: {
password, password,
}); });
await setAuthToken(token as string); await setAuthToken(token as string);
await transferCart();
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customerCacheTag = await getCacheTag("customers"); const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag); revalidateTag(customerCacheTag);
@@ -307,7 +312,12 @@ export async function medusaLoginOrRegister(credentials: {
const customerCacheTag = await getCacheTag("customers"); const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag); revalidateTag(customerCacheTag);
await transferCart();
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customer = await retrieveCustomer(); const customer = await retrieveCustomer();
if (!customer) { if (!customer) {

View File

@@ -14,7 +14,7 @@ export const listProducts = async ({
regionId, regionId,
}: { }: {
pageParam?: number 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 countryCode?: string
regionId?: string regionId?: string
}): Promise<{ }): Promise<{
@@ -63,7 +63,7 @@ export const listProducts = async ({
offset, offset,
region_id: region?.id, region_id: region?.id,
fields: fields:
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags", "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,+status",
...queryParams, ...queryParams,
}, },
headers, headers,

View File

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

View File

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

View File

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

View File

@@ -4,25 +4,19 @@ import { z } from 'zod';
export const SmtpConfigSchema = z.object({ export const SmtpConfigSchema = z.object({
user: z.string({ user: z.string({
description: error: `Please provide the variable EMAIL_USER`,
'This is the email account to send emails from. This is specific to the email provider.', })
required_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({ pass: z.string({
description: 'This is the password for the email account', error: `Please provide the variable EMAIL_PASSWORD`,
required_error: `Please provide the variable EMAIL_PASSWORD`, }).describe('This is the password for the email account'),
}),
host: z.string({ host: z.string({
description: 'This is the SMTP host for the email provider', error: `Please provide the variable EMAIL_HOST`,
required_error: `Please provide the variable EMAIL_HOST`, }).describe('This is the SMTP host for the email provider'),
}),
port: z.number({ port: z.number({
description: error: `Please provide the variable EMAIL_PORT`,
'This is the port for the email provider. Normally 587 or 465.', }).describe('This is the port for the email provider. Normally 587 or 465.'),
required_error: `Please provide the variable EMAIL_PORT`,
}),
secure: z.boolean({ secure: z.boolean({
description: 'This is whether the connection is secure or not', error: `Please provide the variable EMAIL_TLS`,
required_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 const apiKey = z
.string({ .string({
required_error: 'NEXT_PUBLIC_BASELIME_KEY is required', error: 'NEXT_PUBLIC_BASELIME_KEY is required',
description: 'The Baseline API key',
}) })
.describe('The Baseline API key')
.parse(process.env.NEXT_PUBLIC_BASELIME_KEY); .parse(process.env.NEXT_PUBLIC_BASELIME_KEY);
export class BaselimeServerMonitoringService implements MonitoringService { export class BaselimeServerMonitoringService implements MonitoringService {

View File

@@ -6,14 +6,14 @@ import { getLogger } from '@kit/shared/logger';
const EMAIL_SENDER = z const EMAIL_SENDER = z
.string({ .string({
required_error: 'EMAIL_SENDER is required', error: 'EMAIL_SENDER is required',
}) })
.min(1) .min(1)
.parse(process.env.EMAIL_SENDER); .parse(process.env.EMAIL_SENDER);
const PRODUCT_NAME = z const PRODUCT_NAME = z
.string({ .string({
required_error: 'PRODUCT_NAME is required', error: 'PRODUCT_NAME is required',
}) })
.min(1) .min(1)
.parse(process.env.NEXT_PUBLIC_PRODUCT_NAME); .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 { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
import { ApplicationRole } from '@kit/accounts/types/accounts'; import { ApplicationRole } from '@kit/accounts/types/accounts';
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import { useUser } from '@kit/supabase/hooks/use-user'; import { useUser } from '@kit/supabase/hooks/use-user';
import { pathsConfig, featureFlagsConfig } from '@kit/shared/config';
const paths = { const paths = {
home: pathsConfig.app.home, home: pathsConfig.app.home,
admin: pathsConfig.app.admin, admin: pathsConfig.app.admin,

View File

@@ -21,6 +21,7 @@ import {
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { ButtonTooltip } from './ui/button-tooltip'; import { ButtonTooltip } from './ui/button-tooltip';
import { PackageHeader } from './package-header'; import { PackageHeader } from './package-header';
import { pathsConfig } from '../config';
export type AnalysisPackageWithVariant = Pick<StoreProduct, 'title' | 'description' | 'subtitle' | 'metadata'> & { export type AnalysisPackageWithVariant = Pick<StoreProduct, 'title' | 'description' | 'subtitle' | 'metadata'> & {
variantId: string; variantId: string;
@@ -57,7 +58,7 @@ export default function SelectAnalysisPackage({
}); });
setIsAddingToCart(false); setIsAddingToCart(false);
toast.success(<Trans i18nKey={'order-analysis-package:analysisPackageAddedToCart'} />); toast.success(<Trans i18nKey={'order-analysis-package:analysisPackageAddedToCart'} />);
router.push('/home/cart'); router.push(pathsConfig.app.cart);
} catch (e) { } catch (e) {
toast.error(<Trans i18nKey={'order-analysis-package:analysisPackageAddToCartError'} />); toast.error(<Trans i18nKey={'order-analysis-package:analysisPackageAddToCartError'} />);
setIsAddingToCart(false); 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, content,
icon, icon,
}: { }: {
content?: string | null; content?: JSX.Element | string | null;
icon?: JSX.Element; icon?: JSX.Element;
}) { }) {
if (!content) return null; if (!content) return null;
@@ -23,7 +23,7 @@ export function InfoTooltip({
<TooltipTrigger> <TooltipTrigger>
{icon || <Info className="size-4 cursor-pointer" />} {icon || <Info className="size-4 cursor-pointer" />}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{content}</TooltipContent> <TooltipContent className='sm:max-w-[400px]'>{content}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
); );

View File

@@ -6,32 +6,30 @@ const AppConfigSchema = z
.object({ .object({
name: z name: z
.string({ .string({
description: `This is the name of your SaaS. Ex. "Makerkit"`, error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
required_error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
}) })
.describe(`This is the name of your SaaS. Ex. "Makerkit"`)
.min(1), .min(1),
title: z title: z
.string({ .string({
description: `This is the default title tag of your SaaS.`, error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
required_error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
}) })
.describe(`This is the default title tag of your SaaS.`)
.min(1), .min(1),
description: z.string({ description: z.string({
description: `This is the default description of your SaaS.`, error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`,
required_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 locale: z
.string({ .string({
description: `This is the default locale of your SaaS.`, error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
required_error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
}) })
.describe(`This is the default locale of your SaaS.`)
.default('en'), .default('en'),
theme: z.enum(['light', 'dark', 'system']), theme: z.enum(['light', 'dark', 'system']),
production: z.boolean(), production: z.boolean(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,14 +7,14 @@ export function getSupabaseClientKeys() {
return z return z
.object({ .object({
url: z.string({ url: z.string({
description: `This is the URL of your hosted Supabase instance. Please provide the variable NEXT_PUBLIC_SUPABASE_URL.`, error: `Please provide the variable NEXT_PUBLIC_SUPABASE_URL`,
required_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 anonKey: z
.string({ .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.`, error: `Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY`,
required_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), .min(1),
}) })
.parse({ .parse({

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 // this is most likely a session error or the user is not logged in
if (response.error) { if (response.error) {
return undefined; return null;
} }
if (response.data?.user) { if (response.data?.user) {

View File

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

@@ -1,15 +1,15 @@
{ {
"homeTabLabel": "Home", "homeTabLabel": "Home",
"homeTabDescription": "Welcome to your home page", "homeTabDescription": "Welcome to your homepage",
"accountMembers": "Company Members", "accountMembers": "Company Members",
"membersTabDescription": "Here you can manage the members of your company.", "membersTabDescription": "Here you can manage your company members.",
"billingTabLabel": "Billing", "billingTabLabel": "Billing",
"billingTabDescription": "Manage your billing and subscription", "billingTabDescription": "Manage your billing and subscriptions",
"dashboardTabLabel": "Dashboard", "dashboardTabLabel": "Dashboard",
"settingsTabLabel": "Settings", "settingsTabLabel": "Settings",
"profileSettingsTabLabel": "Profile", "profileSettingsTabLabel": "Profile",
"subscriptionSettingsTabLabel": "Subscription", "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.", "settingsTabDescription": "Manage your settings and preferences.",
"emailAddress": "Email Address", "emailAddress": "Email Address",
"password": "Password", "password": "Password",
@@ -18,69 +18,72 @@
"cancel": "Cancel", "cancel": "Cancel",
"clear": "Clear", "clear": "Clear",
"close": "Close", "close": "Close",
"notFound": "Not Found", "notFound": "Not found",
"backToHomePage": "Back to Home Page", "backToHomePage": "Back to homepage",
"goBack": "Go Back", "goBack": "Go back",
"genericServerError": "Sorry, something went wrong.", "genericServerError": "Sorry, something went wrong.",
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.", "genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
"pageNotFound": "Sorry, this page does not exist.", "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.", "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", "anonymousUser": "Anonymous",
"tryAgain": "Try Again", "tryAgain": "Try again",
"theme": "Theme", "theme": "Theme",
"lightTheme": "Light", "lightTheme": "Light",
"darkTheme": "Dark", "darkTheme": "Dark",
"systemTheme": "System", "systemTheme": "System",
"expandSidebar": "Expand Sidebar", "expandSidebar": "Expand sidebar",
"collapseSidebar": "Collapse Sidebar", "collapseSidebar": "Collapse sidebar",
"documentation": "Documentation", "documentation": "Documentation",
"getStarted": "Get Started", "getStarted": "Get started!",
"getStartedWithPlan": "Get Started with {{plan}}", "getStartedWithPlan": "Get started with plan {{plan}}",
"retry": "Retry", "retry": "Retry",
"contactUs": "Contact Us", "contactUs": "Contact us",
"loading": "Loading. Please wait...", "loading": "Loading. Please wait...",
"yourAccounts": "Your Accounts", "yourAccounts": "Your accounts",
"continue": "Continue", "continue": "Continue",
"skip": "Skip", "skip": "Skip",
"signedInAs": "Signed in as", "signedInAs": "Signed in as",
"pageOfPages": "Page {{page}} of {{total}}", "pageOfPages": "Page {{page}} / {{total}}",
"noData": "No data available", "noData": "No data",
"pageNotFoundHeading": "Ouch! :|", "pageNotFoundHeading": "Oops! :|",
"errorPageHeading": "Ouch! :|", "errorPageHeading": "Oops! :|",
"notifications": "Notifications", "notifications": "Notifications",
"noNotifications": "No notifications", "noNotifications": "No notifications",
"justNow": "Just now", "justNow": "Just now",
"newVersionAvailable": "New version available", "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.", "newVersionAvailableDescription": "A new version of the app is available. We recommend refreshing the page to get the latest updates and avoid issues.",
"newVersionSubmitButton": "Reload and Update", "newVersionSubmitButton": "Refresh and update",
"back": "Back", "back": "Back",
"welcome": "Welcome", "welcome": "Welcome",
"shoppingCart": "Shopping cart", "shoppingCart": "Shopping Cart",
"shoppingCartCount": "Shopping cart ({{count}})", "shoppingCartCount": "Shopping Cart ({{count}})",
"search": "Search{{end}}", "search": "Search{{end}}",
"myActions": "My actions", "myActions": "My actions",
"healthPackageComparison": { "healthPackageComparison": {
"label": "Health package comparison", "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." "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": { "routes": {
"home": "Home", "home": "Home",
"overview": "Overview", "overview": "Overview",
"booking": "Booking", "booking": "Book appointment",
"myOrders": "My orders", "myOrders": "My orders",
"analysisResults": "Analysis results", "analysisResults": "Analysis results",
"orderAnalysisPackage": "Telli analüüside pakett", "orderAnalysisPackage": "Order analysis package",
"orderAnalysis": "Order analysis", "orderAnalysis": "Order analysis",
"orderHealthAnalysis": "Telli terviseuuring", "orderHealthAnalysis": "Order health check",
"account": "Account", "account": "Account",
"members": "Members", "members": "Members",
"billing": "Billing", "billing": "Billing",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"settings": "Settings", "settings": "Settings",
"profile": "Profile", "profile": "Profile",
"application": "Application" "application": "Application",
"pickTime": "Pick time",
"preferences": "Preferences",
"security": "Security"
}, },
"roles": { "roles": {
"owner": { "owner": {
@@ -91,31 +94,53 @@
} }
}, },
"otp": { "otp": {
"requestVerificationCode": "Request Verification Code", "requestVerificationCode": "Please request a verification code",
"requestVerificationCodeDescription": "We must verify your identity to continue with this action. We'll send a verification code to the email address {{email}}.", "requestVerificationCodeDescription": "We need to verify your identity before continuing. We will send a code to your email address {{email}}.",
"sendingCode": "Sending Code...", "sendingCode": "Sending code...",
"sendVerificationCode": "Send Verification Code", "sendVerificationCode": "Send verification code",
"enterVerificationCode": "Enter Verification Code", "enterVerificationCode": "Enter verification code",
"codeSentToEmail": "We've sent a verification code to the email address {{email}}.", "codeSentToEmail": "We have sent a code to your email address {{email}}.",
"verificationCode": "Verification Code", "verificationCode": "Verification code",
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email.", "enterCodeFromEmail": "Enter the 6-digit code we sent to your email address.",
"verifying": "Verifying...", "verifying": "Verifying...",
"verifyCode": "Verify Code", "verifyCode": "Verify code",
"requestNewCode": "Request New Code", "requestNewCode": "Request new code",
"errorSendingCode": "Error sending code. Please try again." "errorSendingCode": "Error sending code. Please try again."
}, },
"cookieBanner": { "cookieBanner": {
"title": "Hey, we use cookies 🍪", "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", "reject": "Reject",
"accept": "Accept" "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", "doctor": "Doctor",
"save": "Save", "save": "Save",
"saveAsDraft": "Save as draft", "saveAsDraft": "Save as draft",
"confirm": "Confirm", "confirm": "Confirm",
"previous": "Previous", "previous": "Previous",
"next": "Next", "next": "Next",
"invalidDataError": "Invalid data submitted", "invalidDataError": "Invalid data",
"language": "Language" "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", "respondToQuestion": "Respond",
"gender": "Gender", "gender": "Gender",
"male": "Male", "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", "accountTabLabel": "Konto seaded",
"accountTabDescription": "Manage your account settings", "accountTabDescription": "Halda oma konto seadeid ja e-posti eelistusi.",
"homePage": "Home", "preferencesTabLabel": "Eelistused",
"billingTab": "Billing", "preferencesTabDescription": "Halda oma eelistusi.",
"settingsTab": "Settings", "securityTabLabel": "Turvalisus",
"multiFactorAuth": "Multi-Factor Authentication", "securityTabDescription": "Kaitse oma kontot.",
"multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account", "homePage": "Avaleht",
"updateProfileSuccess": "Profile successfully updated", "billingTab": "Arveldamine",
"updateProfileError": "Encountered an error. Please try again", "settingsTab": "Seaded",
"updatePasswordSuccess": "Password update request successful", "multiFactorAuth": "Mitmefaktoriline autentimine",
"updatePasswordSuccessMessage": "Your password has been successfully updated!", "multiFactorAuthDescription": "Sea üles mitmefaktoriline autentimine, et oma kontot rohkem turvata",
"updatePasswordError": "Encountered an error. Please try again", "updateProfileSuccess": "Profiil edukalt uuendatud",
"updatePasswordLoading": "Updating password...", "updateProfileError": "Ilmnes viga. Palun proovi uuesti",
"updateProfileLoading": "Updating profile...", "updatePasswordSuccess": "Parooli uuendamine õnnestus",
"name": "Your Name", "updatePasswordSuccessMessage": "Sinu parool on edukalt uuendatud!",
"nameDescription": "Update your name to be displayed on your profile", "updatePasswordError": "Ilmnes viga. Palun proovi uuesti",
"emailLabel": "Email Address", "updatePasswordLoading": "Parooli uuendamine...",
"accountImage": "Your Profile Picture", "updateProfileLoading": "Profiili uuendamine...",
"accountImageDescription": "Please choose a photo to upload as your profile picture.", "name": "Sinu nimi",
"profilePictureHeading": "Upload a Profile Picture", "nameDescription": "Uuenda oma nime, mis kuvatakse profiilil",
"profilePictureSubheading": "Choose a photo to upload as your profile picture.", "emailLabel": "E-posti aadress",
"updateProfileSubmitLabel": "Update Profile", "accountImage": "Sinu profiilipilt",
"updatePasswordCardTitle": "Update your Password", "accountImageDescription": "Vali foto, mida soovid profiilipildina üles laadida.",
"updatePasswordCardDescription": "Update your password to keep your account secure.", "profilePictureHeading": "Laadi üles profiilipilt",
"currentPassword": "Current Password", "profilePictureSubheading": "Vali foto, mida soovid profiilipildina üles laadida.",
"newPassword": "New Password", "updateProfileSubmitLabel": "Uuenda profiili",
"repeatPassword": "Repeat New Password", "updatePasswordCardTitle": "Uuenda oma parool",
"repeatPasswordDescription": "Please repeat your new password to confirm it", "updatePasswordCardDescription": "Uuenda oma parooli, et hoida oma konto turvaline.",
"yourPassword": "Your Password", "currentPassword": "Praegune parool",
"updatePasswordSubmitLabel": "Update Password", "newPassword": "Uus parool",
"updateEmailCardTitle": "Update your Email", "repeatPassword": "Korda uut parooli",
"updateEmailCardDescription": "Update your email address you use to login to your account", "repeatPasswordDescription": "Palun korda oma uus parool, et seda kinnitada",
"newEmail": "Your New Email", "yourPassword": "Sinu parool",
"repeatEmail": "Repeat Email", "updatePasswordSubmitLabel": "Uuenda parooli",
"updateEmailSubmitLabel": "Update Email Address", "updateEmailCardTitle": "Uuenda oma e-posti",
"updateEmailSuccess": "Email update request successful", "updateEmailCardDescription": "Uuenda e-posti aadressi, mida kasutad kontole sisselogimiseks",
"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.", "newEmail": "Sinu uus e-post",
"updateEmailLoading": "Updating your email...", "repeatEmail": "Korda e-posti",
"updateEmailError": "Email not updated. Please try again", "updateEmailSubmitLabel": "Uuenda e-posti aadressi",
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password", "updateEmailSuccess": "E-posti uuendamine õnnestus",
"emailNotMatching": "Emails do not match. Make sure you're using the correct email", "updateEmailSuccessMessage": "Saadame sulle kinnituskirja uue e-posti aadressi kinnitamiseks. Palun vaata oma postkasti ja klõpsa lingil.",
"passwordNotChanged": "Your password has not changed", "updateEmailLoading": "E-posti uuendamine...",
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email", "updateEmailError": "E-posti ei uuendatud. Palun proovi uuesti",
"cannotUpdatePassword": "You cannot update your password because your account is not linked to any.", "passwordNotMatching": "Paroolid ei ühti. Veendu, et kasutad õiget parooli",
"setupMfaButtonLabel": "Setup a new Factor", "emailNotMatching": "E-postid ei ühti. Veendu, et kasutad õiget e-posti",
"multiFactorSetupErrorHeading": "Setup Failed", "passwordNotChanged": "Sinu parool ei ole muutunud",
"multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.", "emailsNotMatching": "E-postid ei ühti. Veendu, et kasutad õiget e-posti",
"multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication", "cannotUpdatePassword": "Sa ei saa oma parooli uuendada, kuna sinu kontot ei ole lingitud ühegi parooliga.",
"multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.", "setupMfaButtonLabel": "Sea uus faktor",
"factorNameLabel": "A memorable name to identify this factor", "multiFactorSetupErrorHeading": "Seadistamine ebaõnnestus",
"factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14", "multiFactorSetupErrorDescription": "Vabandame, tekkis viga faktori seadistamisel. Palun proovi uuesti.",
"factorNameSubmitLabel": "Set factor name", "multiFactorAuthHeading": "Turvake oma konto mitmefaktorilise autentimisega",
"unenrollTooltip": "Unenroll this factor", "multiFactorModalHeading": "Kasuta oma autentimisrakendust QR-koodi skannimiseks. Seejärel sisesta genereeritud kood.",
"unenrollingFactor": "Unenrolling factor...", "factorNameLabel": "Meeldejääv nimi faktori tuvastamiseks",
"unenrollFactorSuccess": "Factor successfully unenrolled", "factorNameHint": "Kasuta lihtsat nime, et hiljem seda faktorit kergesti tuvastada. Nt iPhone 14",
"unenrollFactorError": "Unenrolling factor failed", "factorNameSubmitLabel": "Määra faktori nimi",
"factorsListError": "Error loading factors list", "unenrollTooltip": "Tühista selle faktori registreerimine",
"factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.", "unenrollingFactor": "Faktori registreerimine tühistatakse...",
"factorName": "Factor Name", "unenrollFactorSuccess": "Faktor edukalt tühistatud",
"factorType": "Type", "unenrollFactorError": "Faktori tühistamine ebaõnnestus",
"factorStatus": "Status", "factorsListError": "Faktorite nimekirja laadimisel tekkis viga",
"mfaEnabledSuccessTitle": "Multi-Factor authentication is enabled", "factorsListErrorDescription": "Vabandame, ei õnnestunud faktorite nimekirja laadida. Palun proovi uuesti.",
"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.", "factorName": "Faktori nimi",
"verificationCode": "Verification Code", "factorType": "Tüüp",
"addEmailAddress": "Add Email address", "factorStatus": "Staatus",
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above", "mfaEnabledSuccessTitle": "Mitmefaktoriline autentimine on aktiveeritud",
"loadingFactors": "Loading factors...", "mfaEnabledSuccessDescription": "Palju õnne! Sa oled edukalt registreeritud mitmefaktorilise autentimise protsessi. Nüüd pääsed oma kontole parooli ja autentimiskoodi abil.",
"enableMfaFactor": "Enable Factor", "verificationCode": "Kinnituskood",
"disableMfaFactor": "Disable Factor", "addEmailAddress": "Lisa e-posti aadress",
"qrCodeErrorHeading": "QR Code Error", "verifyActivationCodeDescription": "Sisesta 6-kohaline kood, mille sinu autentimisrakendus genereeris",
"qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code", "loadingFactors": "Faktorite laadimine...",
"multiFactorSetupSuccess": "Factor successfully enrolled", "enableMfaFactor": "Luba faktor",
"submitVerificationCode": "Submit Verification Code", "disableMfaFactor": "Keela faktor",
"mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled", "qrCodeErrorHeading": "QR-koodi viga",
"verifyingCode": "Verifying code...", "qrCodeErrorDescription": "Vabandame, QR-koodi genereerimine ebaõnnestus",
"invalidVerificationCodeHeading": "Invalid Verification Code", "multiFactorSetupSuccess": "Faktor edukalt registreeritud",
"invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.", "submitVerificationCode": "Esita kinnituskood",
"unenrollFactorModalHeading": "Unenroll Factor", "mfaEnabledSuccessAlert": "Mitmefaktoriline autentimine on aktiveeritud",
"unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.", "verifyingCode": "Koodi kontrollimine...",
"unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.", "invalidVerificationCodeHeading": "Vale kinnituskood",
"unenrollFactorModalButtonLabel": "Yes, unenroll factor", "invalidVerificationCodeDescription": "Sisestatud kinnituskood ei kehti. Palun proovi uuesti.",
"selectFactor": "Choose a factor to verify your identity", "unenrollFactorModalHeading": "Tühista faktori registreerimine",
"disableMfa": "Disable Multi-Factor Authentication", "unenrollFactorModalDescription": "Sa oled tühistamas selle faktori registreerimist. Sa ei saa seda enam kontole sisselogimiseks kasutada.",
"disableMfaButtonLabel": "Disable MFA", "unenrollFactorModalBody": "Sa oled tühistamas selle faktori registreerimist. Sa ei saa seda enam kontole sisselogimiseks kasutada.",
"confirmDisableMfaButtonLabel": "Yes, disable MFA", "unenrollFactorModalButtonLabel": "Jah, tühista faktor",
"disablingMfa": "Disabling Multi-Factor Authentication. Please wait...", "selectFactor": "Vali faktor, et tuvastada oma identiteet",
"disableMfaSuccess": "Multi-Factor Authentication successfully disabled", "disableMfa": "Keela mitmefaktoriline autentimine",
"disableMfaError": "Sorry, we encountered an error. MFA has not been disabled.", "disableMfaButtonLabel": "Keela MFA",
"sendingEmailVerificationLink": "Sending Email...", "confirmDisableMfaButtonLabel": "Jah, keela MFA",
"sendEmailVerificationLinkSuccess": "Verification link successfully sent", "disablingMfa": "Mitmefaktoriline autentimine keelatakse. Palun oota...",
"sendEmailVerificationLinkError": "Sorry, we weren't able to send you the email", "disableMfaSuccess": "Mitmefaktoriline autentimine edukalt keelatud",
"sendVerificationLinkSubmitLabel": "Send Verification Link", "disableMfaError": "Vabandame, tekkis viga. MFA ei ole keelatud.",
"sendVerificationLinkSuccessLabel": "Email sent! Check your Inbox", "sendingEmailVerificationLink": "E-kirja saatmine...",
"verifyEmailAlertHeading": "Please verify your email to enable MFA", "sendEmailVerificationLinkSuccess": "Kinnituse link edukalt saadetud",
"verificationLinkAlertDescription": "Your email is not yet verified. Please verify your email to be able to set up Multi-Factor Authentication.", "sendEmailVerificationLinkError": "Vabandame, e-kirja saatmine ebaõnnestus",
"authFactorName": "Factor Name (optional)", "sendVerificationLinkSubmitLabel": "Saada kinnituse link",
"authFactorNameHint": "Assign a name that helps you remember the phone number used", "sendVerificationLinkSuccessLabel": "E-post saadetud! Vaata oma postkasti",
"loadingUser": "Loading user details. Please wait...", "verifyEmailAlertHeading": "Palun kinnita oma e-post, et lubada MFA",
"linkPhoneNumber": "Link Phone Number", "verificationLinkAlertDescription": "Sinu e-post ei ole veel kinnitatud. Palun kinnita e-post, et saaksid mitmefaktorilise autentimise seadistada.",
"dangerZone": "Danger Zone", "authFactorName": "Faktori nimi (valikuline)",
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.", "authFactorNameHint": "Määra nimi, mis aitab meenutada kasutatud telefoninumbrit",
"deleteAccount": "Delete your Account", "loadingUser": "Kasutaja andmete laadimine. Palun oota...",
"deletingAccount": "Deleting account. Please wait...", "linkPhoneNumber": "Seosta telefoninumber",
"deleteAccountDescription": "This will delete your account and the accounts you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone.", "dangerZone": "Ohtlik tsoon",
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm", "dangerZoneDescription": "Mõnda toimingut ei saa tagasi võtta. Ole ettevaatlik.",
"deleteAccountErrorHeading": "Sorry, we couldn't delete your account", "deleteAccount": "Kustuta oma konto",
"needsReauthentication": "Reauthentication Required", "deletingAccount": "Konto kustutamine. Palun oota...",
"needsReauthenticationDescription": "You need to reauthenticate to change your password. Please sign out and sign in again to change your password.", "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.",
"language": "Language", "deleteProfileConfirmationInputLabel": "Sisesta KUSTUTA, et kinnitada",
"languageDescription": "Choose your preferred language", "deleteAccountErrorHeading": "Vabandame, me ei saanud sinu kontot kustutada",
"noTeamsYet": "You don't have any teams yet.", "needsReauthentication": "Taastõendamine vajalik",
"createTeam": "Create a team to get started.", "needsReauthenticationDescription": "Sa pead uuesti autentima, et muuta oma parooli. Palun logi välja ja seejärel sisse, et parooli muuta.",
"createTeamButtonLabel": "Create a Team", "language": "Keel",
"languageDescription": "Vali eelistatud keel",
"noTeamsYet": "Sul ei ole veel meeskondi.",
"createTeam": "Loo meeskond alustamiseks.",
"createTeamButtonLabel": "Loo meeskond",
"createCompanyAccount": "Loo ettevõtte konto", "createCompanyAccount": "Loo ettevõtte konto",
"requestCompanyAccount": { "requestCompanyAccount": {
"title": "Ettevõtte andmed", "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", "button": "Küsi pakkumist",
"successTitle": "Päring edukalt saadetud!", "successTitle": "Päring edukalt saadetud!",
"successDescription": "Saadame teile esimesel võimalusel vastuse", "successDescription": "Vastame sulle esimesel võimalusel",
"successButton": "Tagasi kodulehele" "successButton": "Tagasi avalehele"
}, },
"updateAccount": { "updateAccount": {
"title": "Isikuandmed", "title": "Isikuandmed",
@@ -129,7 +133,7 @@
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid" "userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid"
}, },
"consentModal": { "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.", "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", "reject": "Ei anna nõusolekut",
"accept": "Annan nõusoleku" "accept": "Annan nõusoleku"
@@ -152,5 +156,14 @@
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud", "updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud", "updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse...", "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", "signUpHeading": "Loo konto",
"signUp": "Sign Up", "signUp": "Loo konto",
"signUpSubheading": "Fill the form below to create an account.", "signUpSubheading": "Täida allolev vorm, et luua konto.",
"signInHeading": "Sign in to your account", "signInHeading": "Logi oma kontole sisse",
"signInSubheading": "Welcome back! Please enter your details", "signInSubheading": "Tere tulemast tagasi! Palun sisesta oma andmed",
"signIn": "Sign In", "signIn": "Logi sisse",
"getStarted": "Get started", "getStarted": "Alusta",
"updatePassword": "Update Password", "updatePassword": "Uuenda parooli",
"signOut": "Sign out", "signOut": "Logi välja",
"signingIn": "Signing in...", "signingIn": "Sisselogimine...",
"signingUp": "Signing up...", "signingUp": "Registreerimine...",
"doNotHaveAccountYet": "Do not have an account yet?", "doNotHaveAccountYet": "Kas sul pole veel kontot?",
"alreadyHaveAnAccount": "Already have an account?", "alreadyHaveAnAccount": "Kas sul on juba konto?",
"signUpToAcceptInvite": "Please sign in/up to accept the invite", "signUpToAcceptInvite": "Palun logi sisse/registreeru, et kutse vastu võtta",
"clickToAcceptAs": "Click the button below to accept the invite with as <b>{{email}}</b>", "clickToAcceptAs": "Klõpsa alloleval nupul, et võtta kutse vastu kui <b>{{email}}</b>",
"acceptInvite": "Accept invite", "acceptInvite": "Võta kutse vastu",
"acceptingInvite": "Accepting Invite...", "acceptingInvite": "Kutset vastu võttes...",
"acceptInviteSuccess": "Invite successfully accepted", "acceptInviteSuccess": "Kutse on edukalt vastu võetud",
"acceptInviteError": "Error encountered while accepting invite", "acceptInviteError": "Kutse vastuvõtmisel tekkis viga",
"acceptInviteWithDifferentAccount": "Want to accept the invite with a different account?", "acceptInviteWithDifferentAccount": "Soovid kutse vastu võtta teise kontoga?",
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead", "alreadyHaveAccountStatement": "Mul on juba konto, ma tahan sisse logida",
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead", "doNotHaveAccountStatement": "Mul pole kontot, ma tahan registreeruda",
"signInWithProvider": "Sign in with {{provider}}", "signInWithProvider": "Logi sisse teenusega {{provider}}",
"signInWithPhoneNumber": "Sign in with Phone Number", "signInWithPhoneNumber": "Logi sisse telefoninumbriga",
"signInWithEmail": "Sign in with Email", "signInWithEmail": "Logi sisse e-posti aadressiga",
"signUpWithEmail": "Sign up with Email", "signUpWithEmail": "Registreeru e-posti aadressiga",
"passwordHint": "Ensure it's at least 8 characters", "passwordHint": "Veendu, et see oleks vähemalt 8 tähemärki pikk",
"repeatPasswordHint": "Type your password again", "repeatPasswordHint": "Sisesta oma parool uuesti",
"repeatPassword": "Repeat password", "repeatPassword": "Korda parooli",
"passwordForgottenQuestion": "Password forgotten?", "passwordForgottenQuestion": "Unustasid parooli?",
"passwordResetLabel": "Reset Password", "passwordResetLabel": "Taasta parool",
"passwordResetSubheading": "Enter your email address below. You will receive a link to reset your password.", "passwordResetSubheading": "Sisesta oma e-posti aadress alla. Saadame sulle lingi parooli lähtestamiseks.",
"passwordResetSuccessMessage": "Check your Inbox! We emailed you a link for resetting your Password.", "passwordResetSuccessMessage": "Kontrolli oma postkasti! Saatsime sulle parooli lähtestamise lingi.",
"passwordRecoveredQuestion": "Password recovered?", "passwordRecoveredQuestion": "Kas parool on taastatud?",
"passwordLengthError": "Please provide a password with at least 6 characters", "passwordLengthError": "Palun sisesta parool, mis on vähemalt 6 tähemärki pikk",
"sendEmailLink": "Send Email Link", "sendEmailLink": "Saada e-posti link",
"sendingEmailLink": "Sending Email Link...", "sendingEmailLink": "E-posti lingi saatmine...",
"sendLinkSuccessDescription": "Check your email, we just sent you a link. Follow the link to sign in.", "sendLinkSuccessDescription": "Kontrolli oma e-posti, just saatsime sulle lingi. Järgi linki, et sisse logida.",
"sendLinkSuccess": "We sent you a link by email", "sendLinkSuccess": "Saadame sulle lingi e-posti teel",
"sendLinkSuccessToast": "Link successfully sent", "sendLinkSuccessToast": "Link edukalt saadetud",
"getNewLink": "Get a new link", "getNewLink": "Hangi uus link",
"verifyCodeHeading": "Verify your account", "verifyCodeHeading": "Kinnita oma konto",
"verificationCode": "Verification Code", "verificationCode": "Kinnituskood",
"verificationCodeHint": "Enter the code we sent you by SMS", "verificationCodeHint": "Sisesta SMS-iga saadetud kood",
"verificationCodeSubmitButtonLabel": "Submit Verification Code", "verificationCodeSubmitButtonLabel": "Sisesta kinnituskood",
"sendingMfaCode": "Sending Verification Code...", "sendingMfaCode": "Kinnituskoodi saatmine...",
"verifyingMfaCode": "Verifying code...", "verifyingMfaCode": "Koodi kontrollimine...",
"sendMfaCodeError": "Sorry, we couldn't send you a verification code", "sendMfaCodeError": "Vabandust, meil ei õnnestunud saata kinnituskoodi",
"verifyMfaCodeSuccess": "Code verified! Signing you in...", "verifyMfaCodeSuccess": "Kood kinnitatud! Sisselogimine...",
"verifyMfaCodeError": "Ops! It looks like the code is not correct", "verifyMfaCodeError": "Ups! Paistab, et kood ei ole õige",
"reauthenticate": "Reauthenticate", "reauthenticate": "Autentige uuesti",
"reauthenticateDescription": "For security reasons, we need you to re-authenticate", "reauthenticateDescription": "Turvalisuse huvides peame teid uuesti autentima",
"errorAlertHeading": "Sorry, we could not authenticate you", "errorAlertHeading": "Vabandust, me ei saanud sind autentida",
"emailConfirmationAlertHeading": "We sent you a confirmation email.", "emailConfirmationAlertHeading": "Saatsime sulle kinnituskirja e-posti teel.",
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.", "emailConfirmationAlertBody": "Tere tulemast! Palun kontrolli oma e-posti ja klõpsa lingil, et konto kinnitada.",
"resendLink": "Resend link", "resendLink": "Saada link uuesti",
"resendLinkSuccessDescription": "We sent you a new link to your email! Follow the link to sign in.", "resendLinkSuccessDescription": "Saime sulle saata uue lingi! Järgi linki, et sisse logida.",
"resendLinkSuccess": "Check your email!", "resendLinkSuccess": "Kontrolli oma e-posti!",
"authenticationErrorAlertHeading": "Authentication Error", "authenticationErrorAlertHeading": "Autentimise viga",
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.", "authenticationErrorAlertBody": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
"sendEmailCode": "Get code to your Email", "sendEmailCode": "Hangi kood e-posti teel",
"sendingEmailCode": "Sending code...", "sendingEmailCode": "Koodi saatmine...",
"resetPasswordError": "Sorry, we could not reset your password. Please try again.", "resetPasswordError": "Vabandust, me ei saanud parooli lähtestada. Palun proovi uuesti.",
"emailPlaceholder": "your@email.com", "emailPlaceholder": "sinu@email.com",
"inviteAlertHeading": "You have been invited to join a company", "inviteAlertHeading": "Sind on kutsutud ettevõttega liituma",
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.", "inviteAlertBody": "Palun logi sisse või registreeru, et kutse vastu võtta ja ettevõttega liituda.",
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />", "acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
"termsOfService": "Terms of Service", "termsOfService": "Kasutustingimused",
"privacyPolicy": "Privacy Policy", "privacyPolicy": "Privaatsuspoliitika",
"orContinueWith": "Or continue with", "orContinueWith": "Või jätka koos",
"redirecting": "You're in! Please wait...", "redirecting": "Oled sees! Palun oota...",
"errors": { "errors": {
"Invalid login credentials": "The credentials entered are invalid", "Invalid login credentials": "Sisestatud andmed on valed",
"User already registered": "This credential is already in use. Please try with another one.", "User already registered": "See konto on juba kasutusel. Palun proovi teisega.",
"Email not confirmed": "Please confirm your email address before signing in", "Email not confirmed": "Palun kinnita oma e-posti aadress enne sisselogimist",
"default": "We have encountered an error. Please ensure you have a working internet connection and try again", "default": "Tekkis viga. Palun veendu, et sul on töötav internetiühendus, ja proovi uuesti",
"generic": "Sorry, we weren't able to authenticate you. Please try again.", "generic": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
"link": "Sorry, we encountered an error while sending your link. Please try again.", "link": "Vabandust, lingi saatmisel tekkis viga. Palun proovi uuesti.",
"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.", "codeVerifierMismatch": "Paistab, et proovid sisse logida teises brauseris kui see, millest lingi taotlesid. Palun proovi uuesti sama brauseriga.",
"minPasswordLength": "Password must be at least 8 characters long", "minPasswordLength": "Parool peab olema vähemalt 8 tähemärki pikk",
"passwordsDoNotMatch": "The passwords do not match", "passwordsDoNotMatch": "Paroolid ei kattu",
"minPasswordNumbers": "Password must contain at least one number", "minPasswordNumbers": "Parool peab sisaldama vähemalt ühte numbrit",
"minPasswordSpecialChars": "Password must contain at least one special character", "minPasswordSpecialChars": "Parool peab sisaldama vähemalt ühte erimärki",
"uppercasePassword": "Password must contain at least one uppercase letter", "uppercasePassword": "Parool peab sisaldama vähemalt ühte suurtähte",
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action", "insufficient_aal": "Palun logi sisse oma mitmeastmelise autentimisega, et seda toimingut teha",
"otp_expired": "The email link has expired. Please try again.", "otp_expired": "E-posti link on aegunud. Palun proovi uuesti.",
"same_password": "The password cannot be the same as the current password" "same_password": "Parool ei tohi olla sama, mis praegune parool"
} }
} }

View File

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

View File

@@ -1,41 +1,41 @@
{ {
"blog": "Blog", "blog": "Blogi",
"blogSubtitle": "News and updates about the platform", "blogSubtitle": "Uudised ja värskendused platvormi kohta",
"documentation": "Documentation", "documentation": "Dokumentatsioon",
"documentationSubtitle": "Tutorials and guide to get started with the platform", "documentationSubtitle": "Õpetused ja juhendid platvormiga alustamiseks",
"faq": "FAQ", "faq": "KKK",
"faqSubtitle": "Frequently asked questions about the platform", "faqSubtitle": "Korduma kippuvad küsimused platvormi kohta",
"pricing": "Pricing", "pricing": "Hinnakiri",
"pricingSubtitle": "Pricing plans and payment options", "pricingSubtitle": "Hinnaplaanid ja maksevõimalused",
"backToBlog": "Back to blog", "backToBlog": "Tagasi blogisse",
"noPosts": "No posts found", "noPosts": "Postitusi ei leitud",
"blogPaginationNext": "Next Page", "blogPaginationNext": "Järgmine leht",
"blogPaginationPrevious": "Previous Page", "blogPaginationPrevious": "Eelmine leht",
"readMore": "Read more", "readMore": "Loe edasi",
"contactFaq": "If you have any questions, please contact us", "contactFaq": "Kui sul on küsimusi, võta meiega ühendust",
"contact": "Contact", "contact": "Kontakt",
"about": "About", "about": "Meist",
"product": "Product", "product": "Toode",
"legal": "Legal", "legal": "Õiguslik teave",
"termsOfService": "Terms of Service", "termsOfService": "Kasutustingimused",
"termsOfServiceDescription": "Our terms and conditions", "termsOfServiceDescription": "Meie tingimused ja nõuded",
"cookiePolicy": "Cookie Policy", "cookiePolicy": "Küpsiste kasutamise põhimõtted",
"cookiePolicyDescription": "Our cookie policy and how we use them", "cookiePolicyDescription": "Meie küpsiste kasutamise põhimõtted",
"privacyPolicy": "Privacy Policy", "privacyPolicy": "Privaatsuspoliitika",
"privacyPolicyDescription": "Our privacy policy and how we use your data", "privacyPolicyDescription": "Meie privaatsuspoliitika ja kuidas me sinu andmeid kasutame",
"contactDescription": "Contact us for any questions or feedback", "contactDescription": "Võta meiega ühendust küsimuste või tagasiside korral",
"contactHeading": "Send us a message", "contactHeading": "Saada meile sõnum",
"contactSubheading": "We will get back to you as soon as possible", "contactSubheading": "Vastame sulle esimesel võimalusel",
"contactName": "Your Name", "contactName": "Sinu nimi",
"contactEmail": "Your Email", "contactEmail": "Sinu e-post",
"contactMessage": "Your Message", "contactMessage": "Sinu sõnum",
"sendMessage": "Send Message", "sendMessage": "Saada sõnum",
"contactSuccess": "Your message has been sent successfully", "contactSuccess": "Sõnum on edukalt saadetud",
"contactError": "An error occurred while sending your message", "contactError": "Sõnumi saatmisel tekkis viga",
"contactSuccessDescription": "We have received your message and will get back to you as soon as possible", "contactSuccessDescription": "Oleme sinu sõnumi kätte saanud ja võtame sinuga peagi ühendust",
"contactErrorDescription": "An error occurred while sending your message. Please try again later", "contactErrorDescription": "Sõnumi saatmisel tekkis viga. Palun proovi hiljem uuesti",
"footerDescription": "Here you can add a description about your company or product", "footerDescription": "Siia saad lisada oma ettevõtte või toote kirjelduse",
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.", "copyright": "© Copyright {{year}} {{product}}. Kõik õigused kaitstud.",
"heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest", "heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest",
"notInterestedInAudit": "Ei soovi hetkel terviseauditit" "notInterestedInAudit": "Ei soovi hetkel terviseauditit"
} }

View File

@@ -9,20 +9,20 @@
"membersBillingButtonDescription": "Vali kuidas soovid eelarvet töötajate vahel jagada." "membersBillingButtonDescription": "Vali kuidas soovid eelarvet töötajate vahel jagada."
}, },
"settings": { "settings": {
"pageTitle": "Settings", "pageTitle": "Seaded",
"pageDescription": "Manage your Company details", "pageDescription": "Halda oma ettevõtte andmeid",
"teamLogo": "Company Logo", "teamLogo": "Ettevõtte logo",
"teamLogoDescription": "Update your company's logo to make it easier to identify", "teamLogoDescription": "Uuenda oma ettevõtte logo, et seda oleks lihtsam tuvastada",
"teamName": "Company Name", "teamName": "Ettevõtte nimi",
"teamNameDescription": "Update your company's name", "teamNameDescription": "Uuenda oma ettevõtte nime",
"dangerZone": "Danger Zone", "dangerZone": "Ohtlik tsoon",
"dangerZoneDescription": "This section contains actions that are irreversible" "dangerZoneDescription": "See osa sisaldab pöördumatuid toiminguid"
}, },
"members": { "members": {
"pageTitle": "Members" "pageTitle": "Töötajad"
}, },
"billing": { "billing": {
"pageTitle": "Billing" "pageTitle": "Arveldamine"
}, },
"benefitStatistics": { "benefitStatistics": {
"budget": { "budget": {
@@ -50,148 +50,148 @@
"vitaminD": "Vitamiin D", "vitaminD": "Vitamiin D",
"smokers": "Suitsetajad" "smokers": "Suitsetajad"
}, },
"yourTeams": "Your Companies ({{teamsCount}})", "yourTeams": "Sinu ettevõtted ({{teamsCount}})",
"createTeam": "Create a Company", "createTeam": "Loo ettevõte",
"creatingTeam": "Creating Company...", "creatingTeam": "Ettevõtet luuakse...",
"personalAccount": "Personal Account", "personalAccount": "Isiklik konto",
"searchAccount": "Search Account...", "searchAccount": "Otsi kontot...",
"membersTabLabel": "Members", "membersTabLabel": "Töötajad",
"memberName": "Name", "memberName": "Nimi",
"youLabel": "You", "youLabel": "Sina",
"emailLabel": "Email", "emailLabel": "E-post",
"roleLabel": "Role", "roleLabel": "Roll",
"primaryOwnerLabel": "Primary Admin", "primaryOwnerLabel": "Peaadministraator",
"joinedAtLabel": "Joined at", "joinedAtLabel": "Liitus",
"invitedAtLabel": "Invited at", "invitedAtLabel": "Kutsutud",
"inviteMembersPageSubheading": "Invite members to your Company", "inviteMembersPageSubheading": "Kutsu töötajaid oma ettevõttesse",
"createTeamModalHeading": "Create Company", "createTeamModalHeading": "Loo ettevõte",
"createTeamModalDescription": "Create a new Company to manage your projects and members.", "createTeamModalDescription": "Loo uus ettevõte, et hallata oma projekte ja töötajaid.",
"teamNameLabel": "Company Name", "teamNameLabel": "Ettevõtte nimi",
"teamNameDescription": "Your company name should be unique and descriptive", "teamNameDescription": "Ettevõtte nimi peaks olema unikaalne ja kirjeldav",
"createTeamSubmitLabel": "Create Company", "createTeamSubmitLabel": "Loo ettevõte",
"createTeamSuccess": "Company created successfully", "createTeamSuccess": "Ettevõte loodud edukalt",
"createTeamError": "Company not created. Please try again.", "createTeamError": "Ettevõtet ei loodud. Palun proovi uuesti.",
"createTeamLoading": "Creating company...", "createTeamLoading": "Ettevõtet luuakse...",
"settingsPageLabel": "General", "settingsPageLabel": "Üldine",
"createTeamDropdownLabel": "New company", "createTeamDropdownLabel": "Uus ettevõte",
"changeRole": "Change Role", "changeRole": "Muuda rolli",
"removeMember": "Remove from Account", "removeMember": "Eemalda kontolt",
"inviteMembersSuccess": "Members invited successfully!", "inviteMembersSuccess": "Töötajad kutsutud edukalt!",
"inviteMembersError": "Sorry, we encountered an error! Please try again", "inviteMembersError": "Vabandust, tekkis viga! Palun proovi uuesti",
"inviteMembersLoading": "Inviting members...", "inviteMembersLoading": "Töötajaid kutsutakse...",
"removeInviteButtonLabel": "Remove invite", "removeInviteButtonLabel": "Eemalda kutse",
"addAnotherMemberButtonLabel": "Add another one", "addAnotherMemberButtonLabel": "Lisa veel üks",
"inviteMembersButtonLabel": "Send Invites", "inviteMembersButtonLabel": "Saada kutsed",
"removeMemberModalHeading": "You are removing this user", "removeMemberModalHeading": "Sa eemaldad selle kasutaja",
"removeMemberModalDescription": "Remove this member from the company. They will no longer have access to the company.", "removeMemberModalDescription": "Eemalda see töötaja ettevõttest. Tal ei ole enam ligipääsu ettevõttele.",
"removeMemberSuccessMessage": "Member removed successfully", "removeMemberSuccessMessage": "Töötaja eemaldatud edukalt",
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again", "removeMemberErrorMessage": "Vabandust, tekkis viga. Palun proovi uuesti",
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.", "removeMemberErrorHeading": "Vabandust, ei saanud valitud töötajat eemaldada.",
"removeMemberLoadingMessage": "Removing member...", "removeMemberLoadingMessage": "Töötajat eemaldatakse...",
"removeMemberSubmitLabel": "Remove User from Company", "removeMemberSubmitLabel": "Eemalda töötaja ettevõttest",
"chooseDifferentRoleError": "Role is the same as the current one", "chooseDifferentRoleError": "Roll on sama mis praegune",
"updateRole": "Update Role", "updateRole": "Uuenda rolli",
"updateRoleLoadingMessage": "Updating role...", "updateRoleLoadingMessage": "Rolli uuendatakse...",
"updateRoleSuccessMessage": "Role updated successfully", "updateRoleSuccessMessage": "Roll edukalt uuendatud",
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.", "updatingRoleErrorMessage": "Vabandust, tekkis viga. Palun proovi uuesti.",
"updateMemberRoleModalHeading": "Update Member's Role", "updateMemberRoleModalHeading": "Uuenda töötaja rolli",
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.", "updateMemberRoleModalDescription": "Muuda valitud töötaja rolli. Roll määrab töötaja õigused.",
"roleMustBeDifferent": "Role must be different from the current one", "roleMustBeDifferent": "Roll peab erinema praegusest",
"memberRoleInputLabel": "Member role", "memberRoleInputLabel": "Töötaja roll",
"updateRoleDescription": "Pick a role for this member.", "updateRoleDescription": "Vali töötajale roll.",
"updateRoleSubmitLabel": "Update Role", "updateRoleSubmitLabel": "Uuenda rolli",
"transferOwnership": "Transfer Ownership", "transferOwnership": "Anna omandiõigus üle",
"transferOwnershipDescription": "Transfer ownership of the company account to another member.", "transferOwnershipDescription": "Anna ettevõtte konto omandiõigus teisele töötajale.",
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.", "transferOwnershipInputLabel": "Palun kirjuta TRANSFER, et kinnitada omandiõiguse üleandmist.",
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary admin of the company account.", "transferOwnershipInputDescription": "Kui annad omandiõiguse üle, ei ole sa enam ettevõtte konto peaadministraator.",
"deleteInvitation": "Delete Invitation", "deleteInvitation": "Kustuta kutse",
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.", "deleteInvitationDialogDescription": "Oled kustutamas kutset. Kasutaja ei saa enam ettevõtte kontoga liituda.",
"deleteInviteSuccessMessage": "Invite deleted successfully", "deleteInviteSuccessMessage": "Kutse edukalt kustutatud",
"deleteInviteErrorMessage": "Invite not deleted. Please try again.", "deleteInviteErrorMessage": "Kutse ei kustutatud. Palun proovi uuesti.",
"deleteInviteLoadingMessage": "Deleting invite. Please wait...", "deleteInviteLoadingMessage": "Kutset kustutatakse. Palun oota...",
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>", "confirmDeletingMemberInvite": "Oled kustutamas kutset aadressile <b>{{ email }}</b>",
"transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to <b>{{ member }}</b>.", "transferOwnershipDisclaimer": "Sa annad valitud ettevõtte konto omandiõiguse üle kasutajale <b>{{ member }}</b>.",
"transferringOwnership": "Transferring ownership...", "transferringOwnership": "Omandiõigust antakse üle...",
"transferOwnershipSuccess": "Ownership successfully transferred", "transferOwnershipSuccess": "Omandiõigus edukalt üle antud",
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.", "transferOwnershipError": "Vabandust, ei saanud valitud töötajale omandiõigust üle anda. Palun proovi uuesti.",
"deleteInviteSubmitLabel": "Delete Invite", "deleteInviteSubmitLabel": "Kustuta kutse",
"youBadgeLabel": "You", "youBadgeLabel": "Sina",
"updateTeamLoadingMessage": "Updating Company...", "updateTeamLoadingMessage": "Ettevõtet uuendatakse...",
"updateTeamSuccessMessage": "Company successfully updated", "updateTeamSuccessMessage": "Ettevõte edukalt uuendatud",
"updateTeamErrorMessage": "Could not update Company. Please try again.", "updateTeamErrorMessage": "Ettevõtet ei saanud uuendada. Palun proovi uuesti.",
"updateLogoErrorMessage": "Could not update Logo. Please try again.", "updateLogoErrorMessage": "Logo ei saanud uuendada. Palun proovi uuesti.",
"teamNameInputLabel": "Company Name", "teamNameInputLabel": "Ettevõtte nimi",
"teamLogoInputHeading": "Upload your company's Logo", "teamLogoInputHeading": "Laadi üles oma ettevõtte logo",
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.", "teamLogoInputSubheading": "Palun vali pilt, mida kasutada ettevõtte logona.",
"updateTeamSubmitLabel": "Update Company", "updateTeamSubmitLabel": "Uuenda ettevõtet",
"inviteMembersHeading": "Invite Members to your Company", "inviteMembersHeading": "Kutsu töötajaid oma ettevõttesse",
"inviteMembersDescription": "Invite member to your company by entering their email and role.", "inviteMembersDescription": "Kutsu töötajaid oma ettevõttesse sisestades nende e-posti ja rolli.",
"emailPlaceholder": "member@email.com", "emailPlaceholder": "tootaja@email.com",
"membersPageHeading": "Members", "membersPageHeading": "Töötajad",
"inviteMembersButton": "Invite Members", "inviteMembersButton": "Kutsu töötajaid",
"invitingMembers": "Inviting members...", "invitingMembers": "Töötajaid kutsutakse...",
"inviteMembersSuccessMessage": "Members invited successfully", "inviteMembersSuccessMessage": "Töötajad kutsutud edukalt",
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.", "inviteMembersErrorMessage": "Vabandust, töötajaid ei saanud kutsuda. Palun proovi uuesti.",
"pendingInvitesHeading": "Pending Invites", "pendingInvitesHeading": "Ootel kutsed",
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.", "pendingInvitesDescription": " Siin saad hallata oma ettevõtte ootel kutseid.",
"noPendingInvites": "No pending invites found", "noPendingInvites": "Ootel kutseid ei leitud",
"loadingMembers": "Loading members...", "loadingMembers": "Töötajaid laaditakse...",
"loadMembersError": "Sorry, we couldn't fetch your company's members.", "loadMembersError": "Vabandust, ei saanud laadida ettevõtte töötajaid.",
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited members.", "loadInvitedMembersError": "Vabandust, ei saanud laadida kutsutud töötajaid.",
"loadingInvitedMembers": "Loading invited members...", "loadingInvitedMembers": "Kutsutud töötajaid laaditakse...",
"invitedBadge": "Invited", "invitedBadge": "Kutsutud",
"duplicateInviteEmailError": "You have already entered this email address", "duplicateInviteEmailError": "Oled juba sisestanud selle e-posti aadressi",
"invitingOwnAccountError": "Hey, that's your email!", "invitingOwnAccountError": "Hei, see on sinu e-post!",
"dangerZone": "Danger Zone", "dangerZone": "Ohtlik tsoon",
"dangerZoneSubheading": "Delete or leave your company", "dangerZoneSubheading": "Kustuta või lahku oma ettevõttest",
"deleteTeam": "Delete Company", "deleteTeam": "Kustuta ettevõte",
"deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.", "deleteTeamDescription": "Seda toimingut ei saa tagasi võtta. Kõik ettevõttega seotud andmed kustutatakse.",
"deletingTeam": "Deleting company", "deletingTeam": "Ettevõtet kustutatakse",
"deleteTeamModalHeading": "Deleting Company", "deleteTeamModalHeading": "Ettevõtte kustutamine",
"deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.", "deletingTeamDescription": "Oled kustutamas ettevõtet {{ teamName }}. Seda toimingut ei saa tagasi võtta.",
"deleteTeamInputField": "Type the name of the company to confirm", "deleteTeamInputField": "Sisesta ettevõtte nimi kinnituseks",
"leaveTeam": "Leave Company", "leaveTeam": "Lahku ettevõttest",
"leavingTeamModalHeading": "Leaving Company", "leavingTeamModalHeading": "Ettevõttest lahkumine",
"leavingTeamModalDescription": "You are about to leave this company. You will no longer have access to it.", "leavingTeamModalDescription": "Oled lahkumas ettevõttest. Sul ei ole enam sellele ligipääsu.",
"leaveTeamDescription": "Click the button below to leave the company. Remember, you will no longer have access to it and will need to be re-invited to join.", "leaveTeamDescription": "Vajuta allolevat nuppu, et ettevõttest lahkuda. Pea meeles, et sul ei ole enam ligipääsu ja pead uuesti kutsutuks saama.",
"deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.", "deleteTeamDisclaimer": "Oled kustutamas ettevõtet {{ teamName }}. Seda toimingut ei saa tagasi võtta.",
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.", "leaveTeamDisclaimer": "Oled lahkumas ettevõttest {{ teamName }}. Sul ei ole enam sellele ligipääsu.",
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.", "deleteTeamErrorHeading": "Vabandust, ettevõtet ei saanud kustutada.",
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.", "leaveTeamErrorHeading": "Vabandust, ettevõttest ei saanud lahkuda.",
"searchMembersPlaceholder": "Search members", "searchMembersPlaceholder": "Otsi töötajaid",
"createTeamErrorHeading": "Sorry, we couldn't create your company.", "createTeamErrorHeading": "Vabandust, ettevõtet ei saanud luua.",
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.", "createTeamErrorMessage": "Ettevõtet luues tekkis viga. Palun proovi uuesti.",
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.", "transferTeamErrorHeading": "Vabandust, ei saanud ettevõtte omandiõigust üle anda.",
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.", "transferTeamErrorMessage": "Ettevõtte omandiõiguse üleandmisel tekkis viga. Palun proovi uuesti.",
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.", "updateRoleErrorHeading": "Vabandust, ei saanud valitud töötaja rolli uuendada.",
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.", "updateRoleErrorMessage": "Töötaja rolli uuendamisel tekkis viga. Palun proovi uuesti.",
"searchInvitations": "Search Invitations", "searchInvitations": "Otsi kutseid",
"updateInvitation": "Update Invitation", "updateInvitation": "Uuenda kutset",
"removeInvitation": "Remove Invitation", "removeInvitation": "Eemalda kutse",
"acceptInvitation": "Accept Invitation", "acceptInvitation": "Võta kutse vastu",
"renewInvitation": "Renew Invitation", "renewInvitation": "Uuenda kutset",
"resendInvitation": "Resend Invitation", "resendInvitation": "Saada kutse uuesti",
"expiresAtLabel": "Expires at", "expiresAtLabel": "Aegub",
"expired": "Expired", "expired": "Aegunud",
"active": "Active", "active": "Aktiivne",
"inviteStatus": "Status", "inviteStatus": "Staatus",
"inviteNotFoundOrExpired": "Invite not found or expired", "inviteNotFoundOrExpired": "Kutse ei leitud või on aegunud",
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company admin to renew the invite.", "inviteNotFoundOrExpiredDescription": "Kutse, mida otsid, on kas aegunud või ei eksisteeri. Palun võta ühendust ettevõtte administraatoriga, et kutset uuendada.",
"backToHome": "Back to Home", "backToHome": "Tagasi avalehele",
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.", "renewInvitationDialogDescription": "Oled uuendamas kutset aadressile {{ email }}. Kasutaja saab ettevõttega liituda.",
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.", "renewInvitationErrorTitle": "Vabandust, ei saanud kutset uuendada.",
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.", "renewInvitationErrorDescription": "Kutset uuendades tekkis viga. Palun proovi uuesti.",
"signInWithDifferentAccount": "Sign in with a different account", "signInWithDifferentAccount": "Logi sisse teise kontoga",
"signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.", "signInWithDifferentAccountDescription": "Kui soovid kutse vastu võtta teise kontoga, logi välja ja tagasi sisse soovitud kontoga.",
"acceptInvitationHeading": "Accept Invitation to join {{accountName}}", "acceptInvitationHeading": "Võta kutse vastu, et liituda ettevõttega {{accountName}}",
"acceptInvitationDescription": "You have been invited to join the company {{accountName}}. If you wish to accept the invitation, please click the button below.", "acceptInvitationDescription": "Sind on kutsutud liituma ettevõttega {{accountName}}. Kui soovid kutse vastu võtta, vajuta allolevat nuppu.",
"continueAs": "Continue as {{email}}", "continueAs": "Jätka kui {{email}}",
"joinTeamAccount": "Join Company", "joinTeamAccount": "Liitu ettevõttega",
"joiningTeam": "Joining company...", "joiningTeam": "Ettevõttega liitumine...",
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.", "leaveTeamInputLabel": "Palun kirjuta LEAVE kinnituseks, et ettevõttest lahkuda.",
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.", "leaveTeamInputDescription": "Kui lahkud ettevõttest, ei ole sul enam sellele ligipääsu.",
"reservedNameError": "This name is reserved. Please choose a different one.", "reservedNameError": "See nimi on reserveeritud. Palun vali mõni teine.",
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.", "specialCharactersError": "Nimi ei tohi sisaldada erimärke. Palun vali mõni teine.",
"personalCode": "Isikukood", "personalCode": "Isikukood",
"teamOwnerPersonalCodeLabel": "Omaniku isikukood" "teamOwnerPersonalCodeLabel": "Omaniku isikukood"
} }

View File

@@ -1,145 +1,149 @@
{ {
"accountTabLabel": "Настройки аккаунта", "accountTabLabel": "Настройки аккаунта",
"accountTabDescription": "Управляйте настройками вашего аккаунта", "accountTabDescription": "Управляйте настройками аккаунта и предпочтениями электронной почты.",
"preferencesTabLabel": "Предпочтения",
"preferencesTabDescription": "Управляйте своими предпочтениями.",
"securityTabLabel": "Безопасность",
"securityTabDescription": "Защитите свой аккаунт.",
"homePage": "Главная", "homePage": "Главная",
"billingTab": "Оплата", "billingTab": "Оплата",
"settingsTab": "Настройки", "settingsTab": "Настройки",
"multiFactorAuth": "Многофакторная аутентификация", "multiFactorAuth": "Многофакторная аутентификация",
"multiFactorAuthDescription": "Настройте метод многофакторной аутентификации для дополнительной защиты вашего аккаунта", "multiFactorAuthDescription": "Настройте многофакторную аутентификацию для лучшей защиты аккаунта",
"updateProfileSuccess": "Профиль успешно обновлен", "updateProfileSuccess": "Профиль успешно обновлён",
"updateProfileError": "Произошла ошибка. Пожалуйста, попробуйте снова", "updateProfileError": "Произошла ошибка. Пожалуйста, попробуйте снова",
"updatePasswordSuccess": "Запрос на обновление пароля выполнен успешно", "updatePasswordSuccess": "Пароль успешно обновлён",
"updatePasswordSuccessMessage": "Ваш пароль был успешно обновлен!", "updatePasswordSuccessMessage": "Ваш пароль был успешно обновлён!",
"updatePasswordError": "Произошла ошибка. Пожалуйста, попробуйте снова", "updatePasswordError": "Произошла ошибка. Пожалуйста, попробуйте снова",
"updatePasswordLoading": "Обновление пароля...", "updatePasswordLoading": "Обновление пароля...",
"updateProfileLoading": "Обновление профиля...", "updateProfileLoading": "Обновление профиля...",
"name": "Ваше имя", "name": "Ваше имя",
"nameDescription": "Обновите ваше имя, которое будет отображаться в профиле", "nameDescription": "Обновите имя, отображаемое в профиле",
"emailLabel": "Адрес электронной почты", "emailLabel": "Электронная почта",
"accountImage": "Ваша фотография профиля", "accountImage": "Ваше фото профиля",
"accountImageDescription": "Выберите фото для загрузки в качестве изображения профиля.", "accountImageDescription": "Выберите фото для загрузки в качестве аватара.",
"profilePictureHeading": "Загрузить фотографию профиля", "profilePictureHeading": "Загрузите фото профиля",
"profilePictureSubheading": "Выберите фото для загрузки в качестве изображения профиля.", "profilePictureSubheading": "Выберите фото для загрузки в качестве аватара.",
"updateProfileSubmitLabel": "Обновить профиль", "updateProfileSubmitLabel": "Обновить профиль",
"updatePasswordCardTitle": "Обновите ваш пароль", "updatePasswordCardTitle": "Обновить пароль",
"updatePasswordCardDescription": "Обновите пароль, чтобы сохранить ваш аккаунт в безопасности.", "updatePasswordCardDescription": "Обновите пароль, чтобы сохранить аккаунт в безопасности.",
"currentPassword": "Текущий пароль", "currentPassword": "Текущий пароль",
"newPassword": "Новый пароль", "newPassword": "Новый пароль",
"repeatPassword": "Повторите новый пароль", "repeatPassword": "Повторите новый пароль",
"repeatPasswordDescription": "Пожалуйста, повторите новый пароль для подтверждения", "repeatPasswordDescription": "Пожалуйста, повторите новый пароль для подтверждения",
"yourPassword": "Ваш пароль", "yourPassword": "Ваш пароль",
"updatePasswordSubmitLabel": "Обновить пароль", "updatePasswordSubmitLabel": "Обновить пароль",
"updateEmailCardTitle": "Обновите вашу почту", "updateEmailCardTitle": "Обновить электронную почту",
"updateEmailCardDescription": "Обновите адрес электронной почты, который вы используете для входа в аккаунт", "updateEmailCardDescription": "Обновите адрес электронной почты, используемый для входа",
"newEmail": "Новый адрес электронной почты", "newEmail": "Ваш новый email",
"repeatEmail": "Повторите адрес электронной почты", "repeatEmail": "Повторите email",
"updateEmailSubmitLabel": "Обновить адрес электронной почты", "updateEmailSubmitLabel": "Обновить email",
"updateEmailSuccess": "Запрос на обновление почты выполнен успешно", "updateEmailSuccess": "Email успешно обновлён",
"updateEmailSuccessMessage": "Мы отправили вам письмо для подтверждения нового адреса. Пожалуйста, проверьте почту и перейдите по ссылке для подтверждения.", "updateEmailSuccessMessage": "Мы отправим письмо с подтверждением на новый адрес. Проверьте почту и перейдите по ссылке.",
"updateEmailLoading": "Обновление почты...", "updateEmailLoading": "Обновление email...",
"updateEmailError": "Почта не обновлена. Пожалуйста, попробуйте снова", "updateEmailError": "Email не обновлён. Пожалуйста, попробуйте снова",
"passwordNotMatching": "Пароли не совпадают. Убедитесь, что вы используете правильный пароль", "passwordNotMatching": "Пароли не совпадают. Убедитесь, что используете правильный пароль",
"emailNotMatching": "Адреса электронной почты не совпадают. Убедитесь, что вы используете правильный адрес", "emailNotMatching": "Адреса email не совпадают. Убедитесь, что используете правильный email",
"passwordNotChanged": "Ваш пароль не был изменен", "passwordNotChanged": "Ваш пароль не изменён",
"emailsNotMatching": "Адреса электронной почты не совпадают. Убедитесь, что вы используете правильный адрес", "emailsNotMatching": "Адреса email не совпадают. Убедитесь, что используете правильный email",
"cannotUpdatePassword": "Вы не можете обновить пароль, так как ваш аккаунт не связан ни с одним.", "cannotUpdatePassword": "Вы не можете обновить пароль, так как аккаунт не связан с паролем.",
"setupMfaButtonLabel": "Настроить новый фактор", "setupMfaButtonLabel": "Добавить новый фактор",
"multiFactorSetupErrorHeading": "Сбой настройки", "multiFactorSetupErrorHeading": "Ошибка настройки",
"multiFactorSetupErrorDescription": "Извините, произошла ошибка при настройке фактора. Пожалуйста, попробуйте снова.", "multiFactorSetupErrorDescription": "Извините, произошла ошибка при настройке фактора. Попробуйте снова.",
"multiFactorAuthHeading": "Защитите ваш аккаунт с помощью многофакторной аутентификации", "multiFactorAuthHeading": "Защитите аккаунт многофакторной аутентификацией",
"multiFactorModalHeading": "Используйте приложение-аутентификатор для сканирования QR-кода ниже, затем введите сгенерированный код.", "multiFactorModalHeading": "Используйте приложение-аутентификатор для сканирования QR-кода, затем введите сгенерированный код.",
"factorNameLabel": "Запоминающееся имя для идентификации этого фактора", "factorNameLabel": "Удобное имя для идентификации фактора",
"factorNameHint": "Используйте простое для запоминания имя, чтобы легко идентифицировать этот фактор в будущем. Пример: iPhone 14", "factorNameHint": "Используйте простое имя для удобства, например iPhone 14",
"factorNameSubmitLabel": "Задать имя фактора", "factorNameSubmitLabel": "Задать имя фактора",
"unenrollTooltip": "Удалить фактор", "unenrollTooltip": "Удалить фактор",
"unenrollingFactor": "Удаление фактора...", "unenrollingFactor": "Удаление фактора...",
"unenrollFactorSuccess": "Фактор успешно удален", "unenrollFactorSuccess": "Фактор успешно удалён",
"unenrollFactorError": "Не удалось удалить фактор", "unenrollFactorError": "Не удалось удалить фактор",
"factorsListError": "Ошибка загрузки списка факторов", "factorsListError": "Ошибка загрузки списка факторов",
"factorsListErrorDescription": "Извините, не удалось загрузить список факторов. Пожалуйста, попробуйте снова.", "factorsListErrorDescription": "Извините, не удалось загрузить список факторов. Попробуйте снова.",
"factorName": "Имя фактора", "factorName": "Имя фактора",
"factorType": "Тип", "factorType": "Тип",
"factorStatus": "Статус", "factorStatus": "Статус",
"mfaEnabledSuccessTitle": "Многофакторная аутентификация включена", "mfaEnabledSuccessTitle": "Многофакторная аутентификация включена",
"mfaEnabledSuccessDescription": "Поздравляем! Вы успешно подключили многофакторную аутентификацию. Теперь вы сможете входить в аккаунт с помощью комбинации пароля и кода подтверждения, отправленного на ваш номер телефона.", "mfaEnabledSuccessDescription": "Поздравляем! Вы успешно подключили многофакторную аутентификацию. Теперь для входа используйте пароль и код подтверждения.",
"verificationCode": "Код подтверждения", "verificationCode": "Код подтверждения",
"addEmailAddress": "Добавить адрес электронной почты", "addEmailAddress": "Добавить email",
"verifyActivationCodeDescription": "Введите 6-значный код, сгенерированный вашим приложением-аутентификатором, в поле выше", "verifyActivationCodeDescription": "Введите 6-значный код из приложения-аутентификатора",
"loadingFactors": "Загрузка факторов...", "loadingFactors": "Загрузка факторов...",
"enableMfaFactor": "Включить фактор", "enableMfaFactor": "Включить фактор",
"disableMfaFactor": "Отключить фактор", "disableMfaFactor": "Отключить фактор",
"qrCodeErrorHeading": "Ошибка QR-кода", "qrCodeErrorHeading": "Ошибка QR-кода",
"qrCodeErrorDescription": "Извините, не удалось сгенерировать QR-код", "qrCodeErrorDescription": "Извините, не удалось сгенерировать QR-код",
"multiFactorSetupSuccess": "Фактор успешно подключен", "multiFactorSetupSuccess": "Фактор успешно добавлен",
"submitVerificationCode": "Отправить код подтверждения", "submitVerificationCode": "Отправить код подтверждения",
"mfaEnabledSuccessAlert": "Многофакторная аутентификация включена", "mfaEnabledSuccessAlert": "Многофакторная аутентификация включена",
"verifyingCode": "Проверка кода...", "verifyingCode": "Проверка кода...",
"invalidVerificationCodeHeading": "Неверный код подтверждения", "invalidVerificationCodeHeading": "Неверный код подтверждения",
"invalidVerificationCodeDescription": "Введенный вами код неверен. Пожалуйста, попробуйте снова.", "invalidVerificationCodeDescription": "Введённый код недействителен. Попробуйте снова.",
"unenrollFactorModalHeading": "Удаление фактора", "unenrollFactorModalHeading": "Удаление фактора",
"unenrollFactorModalDescription": "Вы собираетесь удалить этот фактор. Вы больше не сможете использовать его для входа в аккаунт.", "unenrollFactorModalDescription": "Вы собираетесь удалить фактор. Его больше нельзя будет использовать для входа.",
"unenrollFactorModalBody": "Вы собираетесь удалить этот фактор. Вы больше не сможете использовать его для входа в аккаунт.", "unenrollFactorModalBody": "Вы собираетесь удалить фактор. Его больше нельзя будет использовать для входа.",
"unenrollFactorModalButtonLabel": "Да, удалить фактор", "unenrollFactorModalButtonLabel": "Да, удалить",
"selectFactor": "Выберите фактор для подтверждения личности", "selectFactor": "Выберите фактор для подтверждения личности",
"disableMfa": "Отключить многофакторную аутентификацию", "disableMfa": "Отключить многофакторную аутентификацию",
"disableMfaButtonLabel": "Отключить MFA", "disableMfaButtonLabel": "Отключить MFA",
"confirmDisableMfaButtonLabel": "Да, отключить MFA", "confirmDisableMfaButtonLabel": "Да, отключить MFA",
"disablingMfa": "Отключение многофакторной аутентификации. Пожалуйста, подождите...", "disablingMfa": "Отключение многофакторной аутентификации. Пожалуйста, подождите...",
"disableMfaSuccess": "Многофакторная аутентификация успешно отключена", "disableMfaSuccess": "Многофакторная аутентификация успешно отключена",
"disableMfaError": "Извините, произошла ошибка. MFA не была отключена.", "disableMfaError": "Извините, произошла ошибка. MFA не отключена.",
"sendingEmailVerificationLink": "Отправка письма...", "sendingEmailVerificationLink": "Отправка письма...",
"sendEmailVerificationLinkSuccess": "Ссылка для подтверждения успешно отправлена", "sendEmailVerificationLinkSuccess": "Ссылка подтверждения успешно отправлена",
"sendEmailVerificationLinkError": "Извините, не удалось отправить письмо", "sendEmailVerificationLinkError": "Извините, отправка письма не удалась",
"sendVerificationLinkSubmitLabel": "Отправить ссылку для подтверждения", "sendVerificationLinkSubmitLabel": "Отправить ссылку подтверждения",
"sendVerificationLinkSuccessLabel": "Письмо отправлено! Проверьте почту", "sendVerificationLinkSuccessLabel": "Email отправлен! Проверьте почту",
"verifyEmailAlertHeading": "Пожалуйста, подтвердите вашу почту, чтобы включить MFA", "verifyEmailAlertHeading": "Пожалуйста, подтвердите email, чтобы включить MFA",
"verificationLinkAlertDescription": "Ваша почта еще не подтверждена. Пожалуйста, подтвердите ее, чтобы настроить многофакторную аутентификацию.", "verificationLinkAlertDescription": "Ваш email ещё не подтверждён. Подтвердите email, чтобы настроить MFA.",
"authFactorName": "Имя фактора (необязательно)", "authFactorName": "Имя фактора (необязательно)",
"authFactorNameHint": "Присвойте имя, которое поможет вам запомнить номер телефона, используемый для входа", "authFactorNameHint": "Задайте имя для удобства запоминания номера телефона",
"loadingUser": "Загрузка данных пользователя. Пожалуйста, подождите...", "loadingUser": "Загрузка данных пользователя. Пожалуйста, подождите...",
"linkPhoneNumber": "Привязать номер телефона", "linkPhoneNumber": "Привязать номер телефона",
"dangerZone": "Опасная зона", "dangerZone": "Опасная зона",
"dangerZoneDescription": "Некоторые действия нельзя отменить. Будьте осторожны.", "dangerZoneDescription": "Некоторые действия нельзя отменить. Будьте осторожны.",
"deleteAccount": "Удалить аккаунт", "deleteAccount": "Удалить аккаунт",
"deletingAccount": "Удаление аккаунта. Пожалуйста, подождите...", "deletingAccount": "Удаление аккаунта. Пожалуйста, подождите...",
"deleteAccountDescription": "Это удалит ваш аккаунт и связанные с ним учетные записи. Также мы немедленно отменим все активные подписки. Это действие нельзя отменить.", "deleteAccountDescription": "Это удалит ваш аккаунт и все аккаунты, владельцем которых вы являетесь. Все активные подписки будут отменены немедленно. Действие необратимо.",
"deleteProfileConfirmationInputLabel": "Введите DELETE для подтверждения", "deleteProfileConfirmationInputLabel": "Введите УДАЛИТЬ для подтверждения",
"deleteAccountErrorHeading": "Извините, не удалось удалить аккаунт", "deleteAccountErrorHeading": "Извините, не удалось удалить аккаунт",
"needsReauthentication": "Требуется повторная аутентификация", "needsReauthentication": "Требуется повторная аутентификация",
"needsReauthenticationDescription": "Необходимо повторно войти в систему, чтобы изменить пароль. Пожалуйста, выйдите и войдите снова.", "needsReauthenticationDescription": "Вам необходимо повторно войти, чтобы изменить пароль. Пожалуйста, выйдите и войдите снова.",
"language": "Язык", "language": "Язык",
"languageDescription": "Выберите предпочитаемый язык", "languageDescription": "Выберите предпочитаемый язык",
"noTeamsYet": "У вас пока нет команд.", "noTeamsYet": "У вас пока нет команд.",
"createTeam": "Создайте команду, чтобы начать.", "createTeam": "Создайте команду, чтобы начать.",
"createTeamButtonLabel": "Создать команду", "createTeamButtonLabel": "Создать команду",
"createCompanyAccount": "Создать аккаунт компании", "createCompanyAccount": "Создать корпоративный аккаунт",
"requestCompanyAccount": { "requestCompanyAccount": {
"title": "Данные компании", "title": "Данные компании",
"description": "Чтобы получить предложение, пожалуйста, введите данные компании, с которой вы планируете использовать MedReport.", "description": "Для получения предложения введите данные компании, с которой планируете использовать MedReport.",
"button": "Запросить предложение", "button": "Запросить предложение",
"successTitle": "Запрос успешно отправлен!", "successTitle": "Запрос успешно отправлен!",
"successDescription": "Мы ответим вам при первой возможности", "successDescription": "Мы ответим вам в ближайшее время",
"successButton": "Вернуться на главную" "successButton": "На главную"
}, },
"updateAccount": { "updateAccount": {
"title": "Личные данные", "title": "Личные данные",
"description": "Пожалуйста, введите свои личные данные для продолжения", "description": "Пожалуйста, введите личные данные для продолжения",
"button": "Продолжить", "button": "Продолжить",
"userConsentLabel": "Я согласен на использование моих персональных данных на платформе", "userConsentLabel": "Я согласен на использование персональных данных на платформе",
"userConsentUrlTitle": "Посмотреть политику обработки персональных данных" "userConsentUrlTitle": "Посмотреть политику конфиденциальности"
}, },
"consentModal": { "consentModal": {
"title": "Прежде чем начать", "title": еред началом",
"description": "Вы даете согласие на использование ваших медицинских данных в анонимной форме для статистики работодателя? Данные будут обезличены и помогут компании лучше поддерживать здоровье сотрудников.", "description": "Вы согласны на использование ваших медицинских данных в анонимном виде в статистике работодателя? Данные останутся обезличенными и помогут лучше поддерживать здоровье сотрудников.",
"reject": "Не даю согласие", "reject": "Не согласен",
"accept": "Да, даю согласие" "accept": "Согласен"
}, },
"updateConsentSuccess": "Согласия обновлены", "updateConsentSuccess": "Согласия обновлены",
"updateConsentError": "Что-то пошло не так. Пожалуйста, попробуйте снова", "updateConsentError": "Что-то пошло не так. Попробуйте снова",
"updateConsentLoading": "Обновление согласий...", "updateConsentLoading": "Обновление согласий...",
"consentToAnonymizedCompanyData": { "consentToAnonymizedCompanyData": {
"label": "Согласен участвовать в статистике работодателя", "label": "Согласен участвовать в статистике работодателя",
"description": "Я согласен на использование моих медицинских данных в анонимной форме для статистики работодателя" "description": "Согласен на использование анонимизированных медицинских данных в статистике работодателя"
}, },
"membershipConfirmation": { "membershipConfirmation": {
"successTitle": "Здравствуйте, {{firstName}} {{lastName}}", "successTitle": "Здравствуйте, {{firstName}} {{lastName}}",
@@ -147,9 +151,19 @@
"successButton": "Продолжить" "successButton": "Продолжить"
}, },
"updateRoleSuccess": "Роль обновлена", "updateRoleSuccess": "Роль обновлена",
"updateRoleError": "Что-то пошло не так. Пожалуйста, попробуйте снова", "updateRoleError": "Что-то пошло не так. Попробуйте снова",
"updateRoleLoading": "Обновление роли...", "updateRoleLoading": "Обновление роли...",
"updatePreferredLocaleSuccess": "Предпочитаемый язык обновлен", "updatePreferredLocaleSuccess": "Предпочитаемый язык обновлён",
"updatePreferredLocaleError": "Не удалось обновить предпочитаемый язык", "updatePreferredLocaleError": "Не удалось обновить предпочитаемый язык",
"updatePreferredLocaleLoading": "Обновление предпочитаемого языка..." "updatePreferredLocaleLoading": "Обновление предпочитаемого языка...",
"doctorAnalysisSummary": "Заключение врача по результатам анализов",
"myHabits": "Мои привычки",
"formField": {
"smoking": "Я курю"
},
"updateAccountSuccess": "Данные аккаунта обновлены",
"updateAccountError": "Не удалось обновить данные аккаунта",
"updateAccountPreferencesSuccess": "Предпочтения аккаунта обновлены",
"updateAccountPreferencesError": "Не удалось обновить предпочтения аккаунта",
"consents": "Согласия"
} }

View File

@@ -1,15 +1,15 @@
{ {
"homeTabLabel": "Главная", "homeTabLabel": "Главная",
"homeTabDescription": "Добро пожаловать на вашу домашнюю страницу", "homeTabDescription": "Добро пожаловать на вашу главную страницу",
"accountMembers": "Члены компании", "accountMembers": "Члены компании",
"membersTabDescription": "Здесь вы можете управлять членами вашей компании.", "membersTabDescription": "Здесь вы можете управлять членами вашей компании.",
"billingTabLabel": "Оплата", "billingTabLabel": "Оплата",
"billingTabDescription": "Управление оплатой и подпиской", "billingTabDescription": "Управляйте своей оплатой и подписками",
"dashboardTabLabel": "Панель", "dashboardTabLabel": "Обзор",
"settingsTabLabel": "Настройки", "settingsTabLabel": "Настройки",
"profileSettingsTabLabel": "Профиль", "profileSettingsTabLabel": "Профиль",
"subscriptionSettingsTabLabel": "Подписка", "subscriptionSettingsTabLabel": "Подписка",
"dashboardTabDescription": "Обзор активности и эффективности вашей учетной записи по всем проектам.", "dashboardTabDescription": "Обзор активности вашей учетной записи и результатов проектов.",
"settingsTabDescription": "Управляйте своими настройками и предпочтениями.", "settingsTabDescription": "Управляйте своими настройками и предпочтениями.",
"emailAddress": "Электронная почта", "emailAddress": "Электронная почта",
"password": "Пароль", "password": "Пароль",
@@ -22,22 +22,22 @@
"backToHomePage": "Вернуться на главную", "backToHomePage": "Вернуться на главную",
"goBack": "Назад", "goBack": "Назад",
"genericServerError": "Извините, что-то пошло не так.", "genericServerError": "Извините, что-то пошло не так.",
"genericServerErrorHeading": "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема не исчезнет.", "genericServerErrorHeading": "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема сохраняется.",
"pageNotFound": "Извините, эта страница не существует.", "pageNotFound": "Извините, эта страница не существует.",
"pageNotFoundSubHeading": "К сожалению, запрашиваемая страница не найдена", "pageNotFoundSubHeading": "Извините, страница, которую вы искали, не найдена",
"genericError": "Извините, что-то пошло не так.", "genericError": "Извините, что-то пошло не так.",
"genericErrorSubHeading": "К сожалению, произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема не исчезнет.", "genericErrorSubHeading": "Произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема сохраняется.",
"anonymousUser": "Анонимный пользователь", "anonymousUser": "Аноним",
"tryAgain": "Попробовать снова", "tryAgain": "Попробовать снова",
"theme": "Тема", "theme": "Тема",
"lightTheme": "Светлая", "lightTheme": "Светлая",
"darkTheme": "Тёмная", "darkTheme": "Темная",
"systemTheme": "Системная", "systemTheme": "Системная",
"expandSidebar": "Развернуть боковое меню", "expandSidebar": "Развернуть боковую панель",
"collapseSidebar": "Свернуть боковое меню", "collapseSidebar": "Свернуть боковую панель",
"documentation": "Документация", "documentation": "Документация",
"getStarted": "Начать!", "getStarted": "Начать!",
"getStartedWithPlan": "Начать с {{plan}}", "getStartedWithPlan": "Начать с плана {{plan}}",
"retry": "Повторить", "retry": "Повторить",
"contactUs": "Свяжитесь с нами", "contactUs": "Свяжитесь с нами",
"loading": "Загрузка. Пожалуйста, подождите...", "loading": "Загрузка. Пожалуйста, подождите...",
@@ -53,8 +53,8 @@
"noNotifications": "Нет уведомлений", "noNotifications": "Нет уведомлений",
"justNow": "Прямо сейчас", "justNow": "Прямо сейчас",
"newVersionAvailable": "Доступна новая версия", "newVersionAvailable": "Доступна новая версия",
"newVersionAvailableDescription": "Доступна новая версия приложения. Рекомендуется обновить страницу, чтобы получить последние обновления и избежать возможных проблем.", "newVersionAvailableDescription": "Доступна новая версия приложения. Рекомендуем обновить страницу, чтобы получить последние обновления и избежать проблем.",
"newVersionSubmitButton": "Перезагрузить и обновить", "newVersionSubmitButton": "Обновить",
"back": "Назад", "back": "Назад",
"welcome": "Добро пожаловать", "welcome": "Добро пожаловать",
"shoppingCart": "Корзина", "shoppingCart": "Корзина",
@@ -63,12 +63,12 @@
"myActions": "Мои действия", "myActions": "Мои действия",
"healthPackageComparison": { "healthPackageComparison": {
"label": "Сравнение пакетов здоровья", "label": "Сравнение пакетов здоровья",
"description": "Ниже приведен персональный выбор пакета медицинского обследования на основе предварительной информации (пол, возраст и индекс массы тела). В таблице можно добавить к рекомендуемому пакету отдельные исследования." "description": "На основе предварительных данных (пол, возраст и индекс массы тела) мы предлагаем персонализированный пакет обследований. В таблице можно добавить дополнительные анализы к рекомендованному пакету."
}, },
"routes": { "routes": {
"home": "Главная", "home": "Главная",
"overview": "Обзор", "overview": "Обзор",
"booking": "Забронировать время", "booking": "Записаться на прием",
"myOrders": "Мои заказы", "myOrders": "Мои заказы",
"analysisResults": "Результаты анализов", "analysisResults": "Результаты анализов",
"orderAnalysisPackage": "Заказать пакет анализов", "orderAnalysisPackage": "Заказать пакет анализов",
@@ -81,33 +81,35 @@
"settings": "Настройки", "settings": "Настройки",
"profile": "Профиль", "profile": "Профиль",
"application": "Приложение", "application": "Приложение",
"pickTime": "Выберите время" "pickTime": "Выбрать время",
"preferences": "Предпочтения",
"security": "Безопасность"
}, },
"roles": { "roles": {
"owner": { "owner": {
"label": "Администратор" "label": "Админ"
}, },
"member": { "member": {
"label": "Участник" "label": "Участник"
} }
}, },
"otp": { "otp": {
"requestVerificationCode": "Запросить код подтверждения", "requestVerificationCode": "Пожалуйста, запросите код подтверждения",
"requestVerificationCodeDescription": "Мы должны подтвердить вашу личность для продолжения. Мы отправим код подтверждения на электронный адрес {{email}}.", "requestVerificationCodeDescription": "Нам нужно подтвердить вашу личность для продолжения. Мы отправим код на адрес {{email}}.",
"sendingCode": "Отправка кода...", "sendingCode": "Отправка кода...",
"sendVerificationCode": "Отправить код подтверждения", "sendVerificationCode": "Отправить код подтверждения",
"enterVerificationCode": "Введите код подтверждения", "enterVerificationCode": "Введите код подтверждения",
"codeSentToEmail": "Мы отправили код подтверждения на электронный адрес {{email}}.", "codeSentToEmail": "Мы отправили код на электронный адрес {{email}}.",
"verificationCode": "Код подтверждения", "verificationCode": "Код подтверждения",
"enterCodeFromEmail": "Введите 6-значный код, который мы отправили на вашу почту.", "enterCodeFromEmail": "Введите 6-значный код, который мы отправили на вашу почту.",
"verifying": "Проверка...", "verifying": "Проверка...",
"verifyCode": "Подтвердить код", "verifyCode": роверить код",
"requestNewCode": "Запросить новый код", "requestNewCode": "Запросить новый код",
"errorSendingCode": "Ошибка при отправке кода. Пожалуйста, попробуйте снова." "errorSendingCode": "Ошибка при отправке кода. Попробуйте еще раз."
}, },
"cookieBanner": { "cookieBanner": {
"title": "Привет, мы используем куки 🍪", "title": "Эй, мы используем куки 🍪",
"description": "Этот сайт использует файлы cookie, чтобы обеспечить вам наилучший опыт.", "description": "Этот сайт использует файлы cookie для обеспечения наилучшего опыта.",
"reject": "Отклонить", "reject": "Отклонить",
"accept": "Принять" "accept": "Принять"
}, },
@@ -118,7 +120,7 @@
"phone": "Телефон", "phone": "Телефон",
"firstName": "Имя", "firstName": "Имя",
"lastName": "Фамилия", "lastName": "Фамилия",
"personalCode": "Личный код", "personalCode": "Персональный код",
"city": "Город", "city": "Город",
"weight": "Вес", "weight": "Вес",
"height": "Рост", "height": "Рост",
@@ -127,7 +129,7 @@
"selectDate": "Выберите дату" "selectDate": "Выберите дату"
}, },
"wallet": { "wallet": {
"balance": "Баланс вашего MedReport аккаунта", "balance": "Баланс вашего счета MedReport",
"expiredAt": "Действительно до {{expiredAt}}" "expiredAt": "Действительно до {{expiredAt}}"
}, },
"doctor": "Врач", "doctor": "Врач",
@@ -136,5 +138,9 @@
"confirm": "Подтвердить", "confirm": "Подтвердить",
"previous": "Предыдущий", "previous": "Предыдущий",
"next": "Следующий", "next": "Следующий",
"invalidDataError": "Некорректные данные" "invalidDataError": "Недопустимые данные",
"language": "Язык",
"yes": "Да",
"no": "Нет",
"preferNotToAnswer": "Предпочитаю не отвечать"
} }

View File

@@ -0,0 +1,7 @@
{
"invalidNumber": "Неверное число",
"invalidEmail": "Неверный email",
"tooShort": "Слишком коротко",
"tooLong": "Слишком длинно",
"invalidPhone": "Неверный телефон"
}

View File

@@ -0,0 +1,43 @@
ALTER TABLE medreport.account_params
ADD is_smoker boolean;
CREATE UNIQUE INDEX params_account_id_pkey ON medreport.account_params USING btree (account_id);
alter table medreport.account_params add constraint "params_account_id_pkey" UNIQUE using index "params_account_id_pkey";
alter policy "users can insert their params"
on "medreport"."account_params"
to authenticated
with check (
account_id in (
select id
from medreport.accounts
where primary_owner_user_id = auth.uid()
)
);
alter policy "users can read their params"
on "medreport"."account_params"
to authenticated
using (
account_id in (
select id
from medreport.accounts
where primary_owner_user_id = auth.uid()
)
);
create policy "users can update their params"
on "medreport"."account_params"
as PERMISSIVE
for UPDATE
to authenticated
using (
account_id in (
select id
from medreport.accounts
where primary_owner_user_id = auth.uid()
)
);