MED-151: add profile view and working smoking dashboard card (#71)

* MED-151: add profile view and working smoking dashboard card

* update zod

* move some components to shared

* move some components to shared

* remove console.logs

* remove unused password form components

* only check null for variant

* use pathsconfig
This commit is contained in:
Helena
2025-09-04 12:17:54 +03:00
committed by GitHub
parent 152ec5f36b
commit 9122acc89f
74 changed files with 4081 additions and 3531 deletions

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,11 @@
import { use } from 'react';
import { cookies } from 'next/headers';
import { retrieveCart } from '@lib/data/cart';
import { StoreCart } from '@medusajs/types';
import { z } from 'zod';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { AppLogo } from '@kit/shared/components/app-logo';
import {
pathsConfig,
personalAccountNavigationConfig,
} from '@kit/shared/config';
import { pathsConfig } from '@kit/shared/config';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
@@ -24,40 +18,11 @@ import { HomeSidebar } from '../_components/home-sidebar';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
function UserHomeLayout({ children }: React.PropsWithChildren) {
const state = use(getLayoutState());
if (state.style === 'sidebar') {
return <SidebarLayout>{children}</SidebarLayout>;
}
return <HeaderLayout>{children}</HeaderLayout>;
}
export default withI18n(UserHomeLayout);
function SidebarLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const state = use(getLayoutState());
return (
<UserWorkspaceContextProvider value={workspace}>
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<HomeSidebar />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} cart={null} />
</PageMobileNavigation>
{children}
</Page>
</SidebarProvider>
</UserWorkspaceContextProvider>
);
}
function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const cart = use(retrieveCart());
@@ -101,27 +66,3 @@ function MobileNavigation({
</>
);
}
async function getLayoutState() {
const cookieStore = await cookies();
const LayoutStyleSchema = z.enum(['sidebar', 'header', 'custom']);
const layoutStyleCookie = cookieStore.get('layout-style');
const sidebarOpenCookie = cookieStore.get('sidebar:state');
const sidebarOpen = sidebarOpenCookie
? sidebarOpenCookie.value === 'false'
: !personalAccountNavigationConfig.sidebarCollapsed;
const parsedStyle = LayoutStyleSchema.safeParse(layoutStyleCookie?.value);
const style = parsedStyle.success
? parsedStyle.data
: personalAccountNavigationConfig.style;
return {
open: sidebarOpen,
style,
};
}

View File

@@ -7,6 +7,7 @@ import { Database } from '@/packages/supabase/src/database.types';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import {
Activity,
ChevronRight,
Clock9,
Droplets,
Pill,
@@ -16,6 +17,7 @@ import {
} from 'lucide-react';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { pathsConfig } from '@kit/shared/config';
import { getPersonParameters } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import {
@@ -24,10 +26,12 @@ import {
CardDescription,
CardFooter,
CardHeader,
CardProps,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { isNil } from 'lodash';
import { BmiCategory } from '~/lib/types/bmi';
import {
bmiFromMetric,
@@ -35,18 +39,26 @@ import {
getBmiStatus,
} from '~/lib/utils';
const getCardVariant = (isSuccess: boolean | null): CardProps['variant'] => {
if (isSuccess === null) return 'default';
if (isSuccess) return 'gradient-success';
return 'gradient-destructive';
};
const cards = ({
gender,
age,
height,
weight,
bmiStatus,
smoking,
}: {
gender?: string;
age?: number;
height?: number | null;
weight?: number | null;
bmiStatus: BmiCategory | null;
smoking?: boolean | null;
}) => [
{
title: 'dashboard:gender',
@@ -102,18 +114,26 @@ const cards = ({
// icon: <LineChart />,
// iconBg: 'bg-success',
// },
// {
// title: 'dashboard:smoking',
// description: 'dashboard:respondToQuestion',
// descriptionColor: 'text-primary',
// icon: (
// <Button size="icon" variant="outline" className="px-2 text-black">
// <ChevronRight className="size-4 stroke-2" />
// </Button>
// ),
// cardVariant: 'gradient-success' as CardProps['variant'],
// },
];
{
title: 'dashboard:smoking',
description:
isNil(smoking)
? 'dashboard:respondToQuestion'
: !!smoking
? 'common:yes'
: 'common:no',
descriptionColor: 'text-primary',
icon:
isNil(smoking) ? (
<Link href={pathsConfig.app.personalAccountSettings}>
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
) : null,
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
},
];
const dummyRecommendations = [
{
@@ -160,8 +180,8 @@ export default function Dashboard({
const params = getPersonParameters(account.personal_code!);
const bmiStatus = getBmiStatus(bmiThresholds, {
age: params?.age || 0,
height: account.account_params?.[0]?.height || 0,
weight: account.account_params?.[0]?.weight || 0,
height: account.accountParams?.height || 0,
weight: account.accountParams?.weight || 0,
});
return (
@@ -170,21 +190,22 @@ export default function Dashboard({
{cards({
gender: params?.gender,
age: params?.age,
height: account.account_params?.[0]?.height,
weight: account.account_params?.[0]?.weight,
height: account.accountParams?.height,
weight: account.accountParams?.weight,
bmiStatus,
smoking: account.accountParams?.isSmoker,
}).map(
({
title,
description,
icon,
iconBg,
// cardVariant,
cardVariant,
// descriptionColor,
}) => (
<Card
key={title}
// variant={cardVariant}
variant={cardVariant}
className="flex flex-col justify-between"
>
<CardHeader className="items-end-safe">
@@ -256,7 +277,7 @@ export default function Dashboard({
</p>
</div>
</div>
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4 min-w-fit">
<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}>

View File

@@ -60,7 +60,7 @@ export async function HomeMenuNavigation(props: {
<span className="flex items-center text-nowrap">{totalValue}</span>
</Button>
)}
<Link href="/home/cart">
<Link href={pathsConfig.app.cart}>
<Button
variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3809
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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