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 const contactEmail = z
.string({ .string({
description: `The email where you want to receive the contact form submissions.`, error:
required_error:
'Contact email is required. Please use the environment variable CONTACT_EMAIL.', 'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
}) }).describe(`The email where you want to receive the contact form submissions.`)
.parse(process.env.CONTACT_EMAIL); .parse(process.env.CONTACT_EMAIL);
const emailFrom = z const emailFrom = z
.string({ .string({
description: `The email sending address.`, error:
required_error:
'Sender email is required. Please use the environment variable EMAIL_SENDER.', 'Sender email is required. Please use the environment variable EMAIL_SENDER.',
}) }).describe(`The email sending address.`)
.parse(process.env.EMAIL_SENDER); .parse(process.env.EMAIL_SENDER);
export const sendContactEmail = enhanceAction( export const sendContactEmail = enhanceAction(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3809
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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