Merge branch 'develop'

This commit is contained in:
2025-09-03 13:32:15 +03:00
90 changed files with 2159 additions and 1024 deletions

15
.env.staging Normal file
View File

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

View File

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

View File

@@ -69,7 +69,7 @@ function AuthButtons() {
</div>
<div className={'flex gap-x-2.5'}>
<Button className={'hidden md:block'} asChild variant={'ghost'}>
<Button className={'block'} asChild variant={'ghost'}>
<Link href={pathsConfig.auth.signIn}>
<Trans i18nKey={'auth:signIn'} />
</Link>

View File

@@ -4,14 +4,15 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { SubmitButton } from '@kit/shared/components/ui/submit-button';
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { sendEmailFromTemplate } from '@/lib/services/mailer.service';
import { CompanySubmitData } from '@/lib/types/company';
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { renderCompanyOfferEmail } from '@kit/email-templates';
import { SubmitButton } from '@kit/shared/components/ui/submit-button';
import { FormItem } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
@@ -39,7 +40,14 @@ const CompanyOfferForm = () => {
});
try {
sendCompanyOfferEmail(data, language)
sendEmailFromTemplate(
renderCompanyOfferEmail,
{
companyData: data,
language,
},
process.env.CONTACT_EMAIL!,
)
.then(() => router.push('/company-offer/success'))
.catch((error) => {
setIsLoading(false);

View File

@@ -0,0 +1,23 @@
import { renderNewJobsAvailableEmail } from '@kit/email-templates';
import { getDoctorAccounts } from '~/lib/services/account.service';
import { getOpenJobAnalysisResponseIds } from '~/lib/services/doctor-jobs.service';
import { sendEmailFromTemplate } from '~/lib/services/mailer.service';
export default async function sendOpenJobsEmails() {
const analysisResponseIds = await getOpenJobAnalysisResponseIds();
const doctorAccounts = await getDoctorAccounts();
const doctorEmails: string[] = doctorAccounts
.map(({ email }) => email)
.filter((email): email is string => !!email);
await sendEmailFromTemplate(
renderNewJobsAvailableEmail,
{
language: 'et',
analysisResponseIds,
},
doctorEmails,
);
}

View File

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

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import {
NotificationAction,
createNotificationLog,
} from '~/lib/services/audit/notificationEntries.service';
import loadEnv from '../handler/load-env';
import sendOpenJobsEmails from '../handler/send-open-jobs-emails';
import validateApiKey from '../handler/validate-api-key';
export const POST = async (request: NextRequest) => {
loadEnv();
try {
validateApiKey(request);
} catch (e) {
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
}
try {
await sendOpenJobsEmails();
console.info(
'Successfully sent out open job notification emails to doctors.',
);
await createNotificationLog({
action: NotificationAction.NEW_JOBS_ALERT,
status: 'SUCCESS',
});
return NextResponse.json(
{
message:
'Successfully sent out open job notification emails to doctors.',
},
{ status: 200 },
);
} catch (e: any) {
console.error(
'Error sending out open job notification emails to doctors.',
e,
);
await createNotificationLog({
action: NotificationAction.NEW_JOBS_ALERT,
status: 'FAIL',
comment: e?.message,
});
return NextResponse.json(
{
message: 'Failed to send out open job notification emails to doctors.',
},
{ status: 500 },
);
}
};

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQueryClient } from '@tanstack/react-query';
import { capitalize } from 'lodash';
import { useForm } from 'react-hook-form';
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
@@ -22,6 +23,9 @@ import {
doctorAnalysisFeedbackFormSchema,
} from '@kit/doctor/schema/doctor-analysis.schema';
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
import {
useCurrentLocaleLanguageNames
} from '@kit/shared/hooks';
import { getFullName } from '@kit/shared/utils';
import { useUser } from '@kit/supabase/hooks/use-user';
import { Button } from '@kit/ui/button';
@@ -57,6 +61,8 @@ export default function AnalysisView({
const { data: user } = useUser();
const languageNames = useCurrentLocaleLanguageNames();
const isInProgress = !!(
!!feedback?.status &&
feedback?.doctor_user_id &&
@@ -191,6 +197,12 @@ export default function AnalysisView({
<Trans i18nKey="doctor:email" />
</div>
<div>{patient.email}</div>
<div className="font-bold">
<Trans i18nKey="common:language" />
</div>
<div>
{capitalize(languageNames.of(patient.preferred_locale ?? 'et'))}
</div>
</div>
<div className="xs:hidden block">
<DoctorJobSelect

View File

@@ -10,6 +10,7 @@ import { Eye } from 'lucide-react';
import { getResultSetName } from '@kit/doctor/lib/helpers';
import { ResponseTable } from '@kit/doctor/schema/doctor-analysis.schema';
import { pathsConfig } from '@kit/shared/config';
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
import { getFullName } from '@kit/shared/utils';
import { useUser } from '@kit/supabase/hooks/use-user';
import { Button } from '@kit/ui/button';
@@ -23,7 +24,9 @@ import {
TableRow,
} from '@kit/ui/table';
import { Trans } from '@kit/ui/trans';
import DoctorJobSelect from './doctor-job-select';
import { capitalize } from 'lodash';
export default function ResultsTable({
results = [],
@@ -58,6 +61,8 @@ export default function ResultsTable({
const [isPending, startTransition] = useTransition();
const { data: currentUser } = useUser();
const languageNames = useCurrentLocaleLanguageNames();
const fetchPage = async (page: number) => {
startTransition(async () => {
const result = await fetchAction({
@@ -116,6 +121,9 @@ export default function ResultsTable({
<TableHead className="w-20">
<Trans i18nKey="doctor:resultsTable.resultsStatus" />
</TableHead>
<TableHead className="w-20">
<Trans i18nKey="doctor:resultsTable.language" />
</TableHead>
<TableHead className="w-20">
<Trans i18nKey="doctor:resultsTable.assignedTo" />
</TableHead>
@@ -179,6 +187,11 @@ export default function ResultsTable({
}}
/>
</TableCell>
<TableCell>
{capitalize(
languageNames.of(result?.patient?.preferred_locale ?? 'et'),
)}
</TableCell>
<TableCell>
<DoctorJobSelect
doctorUserId={result.doctor?.primary_owner_user_id}

View File

@@ -18,8 +18,8 @@ async function AnalysisPage({
id: string;
}>;
}) {
const { id: analysisResponseId } = await params;
const analysisResultDetails = await loadResult(Number(analysisResponseId));
const { id: analysisOrderId } = await params;
const analysisResultDetails = await loadResult(Number(analysisOrderId));
if (!analysisResultDetails) {
return null;
@@ -28,7 +28,7 @@ async function AnalysisPage({
if (analysisResultDetails) {
await createDoctorPageViewLog({
action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
recordKey: analysisResponseId,
recordKey: analysisOrderId,
dataOwnerUserId: analysisResultDetails.patient.userId,
});
}
@@ -50,3 +50,5 @@ async function AnalysisPage({
export default DoctorGuard(AnalysisPage);
const loadResult = cache(getAnalysisResultsForDoctor);

View File

@@ -0,0 +1,107 @@
import Link from 'next/link';
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
import { pathsConfig } from '@kit/shared/config';
import { Button } from '@kit/ui/button';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { loadCurrentUserAccount } from '~/home/(user)/_lib/server/load-user-account';
import { loadUserAnalysis } from '~/home/(user)/_lib/server/load-user-analysis';
import {
PageViewAction,
createPageViewLog,
} from '~/lib/services/audit/pageView.service';
import Analysis from '../_components/analysis';
export default async function AnalysisResultsPage({
params,
}: {
params: Promise<{
id: string;
}>;
}) {
const account = await loadCurrentUserAccount();
const { id: analysisResponseId } = await params;
const analysisResponse = await loadUserAnalysis(Number(analysisResponseId));
if (!account?.id || !analysisResponse) {
return null;
}
await createPageViewLog({
accountId: account.id,
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
});
return (
<>
<PageHeader />
<PageBody className="gap-4">
<div className="mt-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
<div>
<h4>
<Trans i18nKey="analysis-results:pageTitle" />
</h4>
<p className="text-muted-foreground text-sm">
{analysisResponse?.elements &&
analysisResponse.elements?.length > 0 ? (
<Trans i18nKey="analysis-results:description" />
) : (
<Trans i18nKey="analysis-results:descriptionEmpty" />
)}
</p>
</div>
<Button asChild>
<Link href={pathsConfig.app.orderAnalysisPackage}>
<Trans i18nKey="analysis-results:orderNewAnalysis" />
</Link>
</Button>
</div>
<div className="flex flex-col gap-4">
<h4>
<Trans
i18nKey="analysis-results:orderTitle"
values={{ orderNumber: analysisResponse.order.medusa_order_id }}
/>
</h4>
<h5>
<Trans
i18nKey={`orders:status.${analysisResponse.order.status}`}
/>
<ButtonTooltip
content={`${analysisResponse.order.created_at ? new Date(analysisResponse?.order?.created_at).toLocaleString() : ''}`}
className="ml-6"
/>
</h5>
</div>
{analysisResponse?.summary?.value && (
<div>
<strong>
<Trans i18nKey="account:doctorAnalysisSummary" />
</strong>
<p>{analysisResponse.summary.value}</p>
</div>
)}
<div className="flex flex-col gap-2">
{analysisResponse.elements ? (
analysisResponse.elements.map((element, index) => (
<Analysis
key={index}
analysisElement={{ analysis_name_lab: element.analysis_name }}
results={element}
/>
))
) : (
<div className="text-muted-foreground text-sm">
<Trans i18nKey="analysis-results:noAnalysisElements" />
</div>
)}
</div>
</PageBody>
</>
);
}

View File

@@ -1,131 +0,0 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { withI18n } from '@/lib/i18n/with-i18n';
import { Trans } from '@kit/ui/makerkit/trans';
import { PageBody } from '@kit/ui/page';
import { Button } from '@kit/ui/shadcn/button';
import { pathsConfig } from '@kit/shared/config';
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
import {
PageViewAction,
createPageViewLog,
} from '~/lib/services/audit/pageView.service';
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
import { loadUserAnalysis } from '../../_lib/server/load-user-analysis';
import Analysis from './_components/analysis';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('analysis-results:pageTitle');
return {
title,
};
};
async function AnalysisResultsPage() {
const account = await loadCurrentUserAccount();
if (!account) {
throw new Error('Account not found');
}
const analysisResponses = await loadUserAnalysis();
const analysisResponseElements = analysisResponses?.flatMap(
({ elements }) => elements,
);
const analysisOrders = await getAnalysisOrders().catch(() => null);
if (!analysisOrders) {
redirect(pathsConfig.auth.signIn);
}
await createPageViewLog({
accountId: account.id,
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
});
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
...new Set(analysisOrders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]),
];
const analysisElementIds = getAnalysisElementIds(analysisOrders);
const analysisElements = await getAnalysisElements({ ids: analysisElementIds });
return (
<PageBody className="gap-4">
<div className="mt-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-0">
<div>
<h4>
<Trans i18nKey="analysis-results:pageTitle" />
</h4>
<p className="text-muted-foreground text-sm">
{analysisResponses && analysisResponses.length > 0 ? (
<Trans i18nKey="analysis-results:description" />
) : (
<Trans i18nKey="analysis-results:descriptionEmpty" />
)}
</p>
</div>
<Button asChild>
<Link href={pathsConfig.app.orderAnalysisPackage}>
<Trans i18nKey="analysis-results:orderNewAnalysis" />
</Link>
</Button>
</div>
<div className="flex flex-col gap-8">
{analysisOrders.length > 0 && analysisElements.length > 0 ? analysisOrders.map((analysisOrder) => {
const analysisResponse = analysisResponses?.find((response) => response.analysis_order_id === analysisOrder.id);
const analysisElementIds = getAnalysisElementIds([analysisOrder]);
const analysisElementsForOrder = analysisElements.filter((element) => analysisElementIds.includes(element.id));
return (
<div key={analysisOrder.id} className="flex flex-col gap-4">
<h4>
<Trans i18nKey="analysis-results:orderTitle" values={{ orderNumber: analysisOrder.medusa_order_id }} />
</h4>
<h5>
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
<ButtonTooltip
content={`${new Date(analysisOrder.created_at).toLocaleString()}`}
className="ml-6"
/>
</h5>
<div className="flex flex-col gap-2">
{analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => {
const results = analysisResponse?.elements.some((element) => element.analysis_element_original_id === analysisElement.analysis_id_original)
&& analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original);
if (!results) {
return (
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} isCancelled={analysisOrder.status === 'CANCELLED'}/>
);
}
return (
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} results={results} />
);
}) : (
<div className="text-muted-foreground text-sm">
<Trans i18nKey="analysis-results:noAnalysisElements" />
</div>
)}
</div>
</div>
);
}) : (
<div className="text-muted-foreground text-sm">
<Trans i18nKey="analysis-results:noAnalysisOrders" />
</div>
)}
</div>
</PageBody>
);
}
export default withI18n(AnalysisResultsPage);

View File

@@ -1,30 +1,28 @@
import { Trans } from '@kit/ui/trans';
import {
Card,
CardHeader,
CardDescription,
CardFooter,
} from '@kit/ui/card';
import Link from 'next/link';
import { Button } from '@kit/ui/button';
import { ChevronRight, HeartPulse } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { Card, CardDescription, CardFooter, CardHeader } from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
export default function DashboardCards() {
return (
<div className='flex gap-4 lg:px-4'>
<div className="flex gap-4 lg:px-4">
<Card
variant="gradient-success"
className="flex flex-col justify-between"
className="xs:w-1/2 sm:w-auto flex w-full flex-col justify-between"
>
<CardHeader className="flex-row">
<div
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
className={
'bg-primary/10 mb-6 flex size-8 items-center-safe justify-center-safe rounded-full text-white'
}
>
<HeartPulse className="size-4 fill-green-500" />
</div>
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
<Link href='/home/order-analysis'>
<div className="bg-warning ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white">
<Link href="/home/order-analysis">
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
@@ -33,10 +31,10 @@ export default function DashboardCards() {
</CardHeader>
<CardFooter className="flex flex-col items-start gap-2">
<h5>
<Trans i18nKey='dashboard:heroCard.orderAnalysis.title' />
<Trans i18nKey="dashboard:heroCard.orderAnalysis.title" />
</h5>
<CardDescription className="text-primary">
<Trans i18nKey='dashboard:heroCard.orderAnalysis.description' />
<Trans i18nKey="dashboard:heroCard.orderAnalysis.description" />
</CardDescription>
</CardFooter>
</Card>

View File

@@ -166,7 +166,7 @@ export default function Dashboard({
return (
<>
<div className="grid auto-rows-fr grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-5">
<div className="xs:grid-cols-2 grid auto-rows-fr gap-3 sm:grid-cols-4 lg:grid-cols-5">
{cards({
gender: params?.gender,
age: params?.age,
@@ -233,8 +233,11 @@ export default function Dashboard({
index,
) => {
return (
<div className="flex justify-between" key={index}>
<div className="mr-4 flex flex-row items-center gap-4">
<div
className="flex w-full justify-between gap-3 overflow-scroll"
key={index}
>
<div className="mr-4 flex min-w-fit flex-row items-center gap-4">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
@@ -243,7 +246,7 @@ export default function Dashboard({
>
{icon}
</div>
<div>
<div className="min-w-fit">
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
{title}
<InfoTooltip content={tooltipContent} />
@@ -253,16 +256,24 @@ export default function Dashboard({
</p>
</div>
</div>
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4">
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4 min-w-fit">
<p className="text-sm font-medium"> {price}</p>
{href ? (
<Link href={href}>
<Button size="sm" variant="secondary">
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
</Link>
) : (
<Button size="sm" variant="secondary">
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
)}

View File

@@ -1,12 +1,16 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { StoreCart } from '@medusajs/types';
import { LogOut, Menu, ShoppingCart } from 'lucide-react';
import { Cross, LogOut, Menu, Shield, ShoppingCart } from 'lucide-react';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { ApplicationRoleEnum } from '@kit/accounts/types/accounts';
import {
featureFlagsConfig,
pathsConfig,
personalAccountNavigationConfig,
} from '@kit/shared/config';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
@@ -15,7 +19,6 @@ import {
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
@@ -23,14 +26,16 @@ import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
// home imports
import { HomeAccountSelector } from '../_components/home-account-selector';
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
export function HomeMobileNavigation(props: {
workspace: UserWorkspace;
cart: StoreCart | null;
}) {
const user = props.workspace.user;
const signOut = useSignOut();
const { data: personalAccountData } = usePersonalAccountData(user.id);
const Links = personalAccountNavigationConfig.routes.map((item, index) => {
if ('children' in item) {
@@ -51,7 +56,29 @@ export function HomeMobileNavigation(props: {
}
});
const cartQuantityTotal = props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
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 (
@@ -61,22 +88,6 @@ export function HomeMobileNavigation(props: {
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
<If condition={featureFlagsConfig.enableTeamAccounts}>
<DropdownMenuGroup>
<DropdownMenuLabel>
<Trans i18nKey={'common:yourAccounts'} />
</DropdownMenuLabel>
<HomeAccountSelector
userId={props.workspace.user.id}
accounts={props.workspace.accounts}
collisionPadding={0}
/>
</DropdownMenuGroup>
<DropdownMenuSeparator />
</If>
<If condition={props.cart && hasCartItems}>
<DropdownMenuGroup>
<DropdownLink
@@ -91,6 +102,41 @@ export function HomeMobileNavigation(props: {
<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()} />

View File

@@ -52,7 +52,7 @@ export default function OrderAnalysesCards({
}
return (
<div className="grid grid-cols-3 gap-6 mt-4">
<div className="grid 2xs:grid-cols-3 gap-6 mt-4">
{analyses.map(({
title,
variant,

View File

@@ -1,22 +1,32 @@
'use client';
import { Trans } from '@kit/ui/trans';
import { useRouter } from 'next/navigation';
import { StoreOrderLineItem } from '@medusajs/types';
import { formatDate } from 'date-fns';
import { Eye } from 'lucide-react';
import { pathsConfig } from '@kit/shared/config';
import { Button } from '@kit/ui/button';
import {
Table,
TableBody,
TableHead,
TableRow,
TableHeader,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import { StoreOrderLineItem } from "@medusajs/types";
import { Trans } from '@kit/ui/trans';
import { AnalysisOrder } from '~/lib/services/order.service';
import { formatDate } from 'date-fns';
import { Eye } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { logAnalysisResultsNavigateAction } from './actions';
export default function OrderItemsTable({ items, title, analysisOrder }: {
export default function OrderItemsTable({
items,
title,
analysisOrder,
}: {
items: StoreOrderLineItem[];
title: string;
analysisOrder: AnalysisOrder;
@@ -29,11 +39,11 @@ export default function OrderItemsTable({ items, title, analysisOrder }: {
const openAnalysisResults = async () => {
await logAnalysisResultsNavigateAction(analysisOrder.medusa_order_id);
router.push(`/home/analysis-results`);
}
router.push(`${pathsConfig.app.analysisResults}/${analysisOrder.id}`);
};
return (
<Table className="rounded-lg border border-separate">
<Table className="border-separate rounded-lg border">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
<TableRow>
<TableHead className="px-6">
@@ -45,13 +55,14 @@ export default function OrderItemsTable({ items, title, analysisOrder }: {
<TableHead className="px-6">
<Trans i18nKey="orders:table.status" />
</TableHead>
<TableHead className="px-6">
</TableHead>
<TableHead className="px-6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items
.sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1)
.sort((a, b) =>
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
)
.map((orderItem) => (
<TableRow className="w-full" key={orderItem.id}>
<TableCell className="text-left w-[100%] px-6">
@@ -64,23 +75,18 @@ export default function OrderItemsTable({ items, title, analysisOrder }: {
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
</TableCell>
<TableCell className="px-6 min-w-[180px]">
<TableCell className="min-w-[180px] px-6">
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
</TableCell>
<TableCell className="text-right px-6">
<span className="flex gap-x-1 justify-end w-[30px]">
<button
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer "
onClick={openAnalysisResults}
>
<Eye />
</button>
</span>
<TableCell className="px-6 text-right">
<Button size="sm" onClick={openAnalysisResults}>
<Trans i18nKey="analysis-results:view" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
);
}

View File

@@ -6,8 +6,8 @@ import { listRegions } from '@lib/data/regions';
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
import type { StoreProduct } from '@medusajs/types';
import { loadCurrentUserAccount } from './load-user-account';
import { AnalysisPackageWithVariant } from '~/components/select-analysis-package';
import { AccountWithParams } from '@/packages/features/accounts/src/server/api';
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
async function countryCodesLoader() {
const countryCodes = await listRegions().then((regions) =>
@@ -38,8 +38,11 @@ function userSpecificVariantLoader({
if (age >= 18 && age <= 29) {
return '18-29';
}
if (age >= 30 && age <= 49) {
return '30-49';
if (age >= 30 && age <= 39) {
return '30-39';
}
if (age >= 40 && age <= 49) {
return '40-49';
}
if (age >= 50 && age <= 59) {
return '50-59';

View File

@@ -0,0 +1,22 @@
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { UserAnalysis } from '@kit/accounts/types/accounts';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalyses>>;
/**
* @name loadUserAnalyses
* @description
* Load the user's analyses. It's a cached per-request function that fetches the user workspace data.
* It can be used across the server components to load the user workspace data.
*/
export const loadUserAnalyses = cache(analysesLoader);
async function analysesLoader(): Promise<UserAnalysis | null> {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
return api.getUserAnalyses();
}

View File

@@ -1,7 +1,7 @@
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { UserAnalysis } from '@kit/accounts/types/accounts';
import { AnalysisResultDetails } from '@kit/accounts/types/accounts';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
@@ -9,14 +9,15 @@ export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
/**
* @name loadUserAnalysis
* @description
* Load the user's analyses. It's a cached per-request function that fetches the user workspace data.
* It can be used across the server components to load the user workspace data.
* Load the user's analysis based on id. It's a cached per-request function that fetches the user's analysis data.
*/
export const loadUserAnalysis = cache(analysisLoader);
async function analysisLoader(): Promise<UserAnalysis | null> {
async function analysisLoader(
analysisOrderId: number,
): Promise<AnalysisResultDetails | null> {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
return api.getUserAnalysis();
return api.getUserAnalysis(analysisOrderId);
}

View File

@@ -41,3 +41,43 @@ export async function getAccountAdmin({
return data as unknown as AccountWithMemberships;
}
export async function getDoctorAccounts() {
const { data } = await getSupabaseServerAdminClient()
.schema('medreport')
.from('accounts')
.select('id, email, name, last_name, preferred_locale')
.eq('is_personal_account', true)
.eq('application_role', 'doctor')
.throwOnError();
return data?.map(({ id, email, name, last_name, preferred_locale }) => ({
id,
email,
name,
lastName: last_name,
preferredLocale: preferred_locale,
}));
}
export async function getAssignedDoctorAccount(analysisOrderId: number) {
const { data: doctorUser } = await getSupabaseServerAdminClient()
.schema('medreport')
.from('doctor_analysis_feedback')
.select('doctor_user_id')
.eq('analysis_order_id', analysisOrderId)
.throwOnError();
const doctorData = doctorUser[0];
if (!doctorData || !doctorData.doctor_user_id) {
return null;
}
const { data } = await getSupabaseServerAdminClient()
.schema('medreport')
.from('accounts')
.select('email')
.eq('primary_owner_user_id', doctorData.doctor_user_id);
return { email: data?.[0]?.email };
}

View File

@@ -1,8 +1,10 @@
import { Database } from '@kit/supabase/database';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
export enum NotificationAction {
DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED',
NEW_JOBS_ALERT = 'NEW_JOBS_ALERT',
PATIENT_RESULTS_RECEIVED_ALERT = 'PATIENT_RESULTS_RECEIVED_ALERT',
}
export const createNotificationLog = async ({
@@ -17,7 +19,7 @@ export const createNotificationLog = async ({
relatedRecordId?: string | number;
}) => {
try {
const supabase = getSupabaseServerClient();
const supabase = getSupabaseServerAdminClient();
await supabase
.schema('audit')
@@ -30,6 +32,6 @@ export const createNotificationLog = async ({
})
.throwOnError();
} catch (error) {
console.error('Failed to insert doctor page view log', error);
console.error('Failed to insert doctor notification log', error);
}
};

View File

@@ -0,0 +1,32 @@
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
async function getAssignedOrderIds() {
const supabase = getSupabaseServerAdminClient();
const { data: assignedOrderIds } = await supabase
.schema('medreport')
.from('doctor_analysis_feedback')
.select('analysis_order_id')
.not('doctor_user_id', 'is', null)
.throwOnError();
return assignedOrderIds?.map((f) => f.analysis_order_id) || [];
}
export async function getOpenJobAnalysisResponseIds() {
const supabase = getSupabaseServerAdminClient();
const assignedIds = await getAssignedOrderIds();
let query = supabase
.schema('medreport')
.from('analysis_responses')
.select('id, analysis_order_id')
.order('created_at', { ascending: false });
if (assignedIds.length > 0) {
query = query.not('analysis_order_id', 'in', `(${assignedIds.join(',')})`);
}
const { data: analysisResponses } = await query.throwOnError();
return analysisResponses?.map(({ id }) => id) || [];
}

View File

@@ -1,50 +1,41 @@
'use server';
import { CompanySubmitData } from '@/lib/types/company';
import { emailSchema } from '@/lib/validations/email.schema';
import { toArray } from '@/lib/utils';
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
import { getMailer } from '@kit/mailers';
import { enhanceAction } from '@kit/next/actions';
import { getLogger } from '@kit/shared/logger';
export const sendDoctorSummaryCompletedEmail = async (
language: string,
recipientName: string,
recipientEmail: string,
orderNr: string,
orderId: number,
) => {
const { html, subject } = await renderDoctorSummaryReceivedEmail({
language,
recipientName,
recipientEmail,
orderNr,
orderId,
});
import { emailSchema } from '~/lib/validations/email.schema';
await sendEmail({
subject,
html,
to: recipientEmail,
});
type EmailTemplate = {
html: string;
subject: string;
};
export const sendCompanyOfferEmail = async (
data: CompanySubmitData,
language: string,
) => {
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
const { html, subject } = await renderCompanyOfferEmail({
language,
companyData: data,
});
type EmailRenderer<T = any> = (params: T) => Promise<EmailTemplate>;
await sendEmail({
subject,
html,
to: process.env.CONTACT_EMAIL || '',
});
export const sendEmailFromTemplate = async <T>(
renderer: EmailRenderer<T>,
templateParams: T,
recipients: string | string[],
) => {
const { html, subject } = await renderer(templateParams);
const recipientsArray = toArray(recipients);
if (!recipientsArray.length) {
throw new Error('No valid email recipients provided');
}
const emailPromises = recipientsArray.map((email) =>
sendEmail({
subject,
html,
to: email,
}),
);
await Promise.all(emailPromises);
};
export const sendEmail = enhanceAction(
@@ -53,7 +44,7 @@ export const sendEmail = enhanceAction(
const log = await getLogger();
if (!process.env.EMAIL_USER) {
log.error('Sending email failed, as no sender found in env.')
log.error('Sending email failed, as no sender was found in env.');
throw new Error('No email user configured');
}

View File

@@ -97,7 +97,7 @@ export async function getLatestPublicMessageListItem() {
Action: MedipostAction.GetPublicMessageList,
User: USER,
Password: PASSWORD,
Sender: 'syndev',
Sender: RECIPIENT,
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
// MessageType check only for messages of certain type
},

View File

@@ -165,6 +165,20 @@ async function doctorMiddleware(request: NextRequest, response: NextResponse) {
*/
function getPatterns() {
return [
{
pattern: new URLPattern({ pathname: '/' }),
handler: async (req: NextRequest, res: NextResponse) => {
const {
data: { user },
} = await getUser(req, res);
if (user) {
return NextResponse.redirect(
new URL(pathsConfig.app.home, req.nextUrl.origin).href,
);
}
},
},
{
pattern: new URLPattern({ pathname: '/admin/*?' }),
handler: adminMiddleware,

View File

@@ -1,7 +1,20 @@
import { SupabaseClient } from '@supabase/supabase-js';
import {
renderAllResultsReceivedEmail,
renderFirstResultsReceivedEmail,
} from '@kit/email-templates';
import { Database } from '@kit/supabase/database';
import {
getAssignedDoctorAccount,
getDoctorAccounts,
} from '../../../../../lib/services/account.service';
import {
NotificationAction,
createNotificationLog,
} from '../../../../../lib/services/audit/notificationEntries.service';
import { sendEmailFromTemplate } from '../../../../../lib/services/mailer.service';
import { RecordChange, Tables } from '../record-change.type';
export function createDatabaseWebhookRouterService(
@@ -42,6 +55,12 @@ class DatabaseWebhookRouterService {
return this.handleAccountsWebhook(payload);
}
case 'analysis_orders': {
const payload = body as RecordChange<typeof body.table>;
return this.handleAnalysisOrdersWebhook(payload);
}
default: {
return;
}
@@ -83,4 +102,69 @@ class DatabaseWebhookRouterService {
return service.handleAccountDeletedWebhook(body.old_record);
}
}
private async handleAnalysisOrdersWebhook(
body: RecordChange<'analysis_orders'>,
) {
if (body.type === 'UPDATE' && body.record && body.old_record) {
const { record, old_record } = body;
if (record.status === old_record.status) {
return;
}
let action;
try {
const data = {
analysisOrderId: record.id,
language: 'et',
};
if (record.status === 'PARTIAL_ANALYSIS_RESPONSE') {
action = NotificationAction.NEW_JOBS_ALERT;
const doctorAccounts = await getDoctorAccounts();
const doctorEmails: string[] = doctorAccounts
.map(({ email }) => email)
.filter((email): email is string => !!email);
await sendEmailFromTemplate(
renderFirstResultsReceivedEmail,
data,
doctorEmails,
);
} else if (record.status === 'FULL_ANALYSIS_RESPONSE') {
action = NotificationAction.PATIENT_RESULTS_RECEIVED_ALERT;
const doctorAccount = await getAssignedDoctorAccount(record.id);
const assignedDoctorEmail = doctorAccount?.email;
if (!assignedDoctorEmail) {
return;
}
await sendEmailFromTemplate(
renderAllResultsReceivedEmail,
data,
assignedDoctorEmail,
);
}
if (action) {
await createNotificationLog({
action,
status: 'SUCCESS',
relatedRecordId: record.id,
});
}
} catch (e: any) {
if (action)
await createNotificationLog({
action,
status: 'FAIL',
comment: e?.message,
relatedRecordId: record.id,
});
}
}
}
}

View File

@@ -0,0 +1,82 @@
import {
Body,
Head,
Html,
Preview,
Tailwind,
Text,
render
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import CommonFooter from '../components/common-footer';
import { EmailContent } from '../components/content';
import { EmailButton } from '../components/email-button';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
export async function renderAllResultsReceivedEmail({
language,
analysisOrderId,
}: {
language: string;
analysisOrderId: number;
}) {
const namespace = 'all-results-received-email';
const { t } = await initializeEmailI18n({
language,
namespace: [namespace, 'common'],
});
const previewText = t(`${namespace}:previewText`);
const subject = t(`${namespace}:subject`);
const html = await render(
<Html>
<Head>
<BodyStyle />
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body>
<EmailWrapper>
<EmailHeader>
<EmailHeading>{previewText}</EmailHeading>
</EmailHeader>
<EmailContent>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:hello`)}
</Text>
<Text className="text-[16px] leading-[24px] font-semibold text-[#242424]">
{t(`${namespace}:openOrdersHeading`)}
</Text>
<EmailButton
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
>
{t(`${namespace}:linkText`)}
</EmailButton>
<Text>
{t(`${namespace}:ifLinksDisabled`)}{' '}
{`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
</Text>
<CommonFooter t={t} />
</EmailContent>
</EmailWrapper>
</Body>
</Tailwind>
</Html>,
);
return {
html,
subject,
};
}

View File

@@ -19,16 +19,14 @@ import { initializeEmailI18n } from '../lib/i18n';
export async function renderDoctorSummaryReceivedEmail({
language,
recipientEmail,
recipientName,
orderNr,
orderId,
analysisOrderId,
}: {
language?: string;
recipientName: string;
recipientEmail: string;
orderNr: string;
orderId: number;
analysisOrderId: number;
}) {
const namespace = 'doctor-summary-received-email';
@@ -37,8 +35,6 @@ export async function renderDoctorSummaryReceivedEmail({
namespace: [namespace, 'common'],
});
const to = recipientEmail;
const previewText = t(`${namespace}:previewText`, {
orderNr,
});
@@ -73,13 +69,13 @@ export async function renderDoctorSummaryReceivedEmail({
</Text>
<EmailButton
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
>
{t(`${namespace}:linkText`, { orderNr })}
</EmailButton>
<Text>
{t(`${namespace}:ifButtonDisabled`)}{' '}
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
</Text>
<CommonFooter t={t} />
</EmailContent>
@@ -92,6 +88,5 @@ export async function renderDoctorSummaryReceivedEmail({
return {
html,
subject,
to,
};
}

View File

@@ -0,0 +1,86 @@
import {
Body,
Head,
Html,
Preview,
Tailwind,
Text,
render,
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import CommonFooter from '../components/common-footer';
import { EmailContent } from '../components/content';
import { EmailButton } from '../components/email-button';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
export async function renderFirstResultsReceivedEmail({
language,
analysisOrderId,
}: {
language: string;
analysisOrderId: number;
}) {
const namespace = 'first-results-received-email';
const { t } = await initializeEmailI18n({
language,
namespace: [namespace, 'common'],
});
const previewText = t(`${namespace}:previewText`);
const subject = t(`${namespace}:subject`);
const html = await render(
<Html>
<Head>
<BodyStyle />
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body>
<EmailWrapper>
<EmailHeader>
<EmailHeading>{previewText}</EmailHeading>
</EmailHeader>
<EmailContent>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:hello`)}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:resultsReceivedForOrders`)}
</Text>
<Text className="text-[16px] leading-[24px] font-semibold text-[#242424]">
{t(`${namespace}:openOrdersHeading`)}
</Text>
<EmailButton
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
>
{t(`${namespace}:linkText`)}
</EmailButton>
<Text>
{t(`${namespace}:ifLinksDisabled`)}{' '}
{`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`}
</Text>
<CommonFooter t={t} />
</EmailContent>
</EmailWrapper>
</Body>
</Tailwind>
</Html>,
);
return {
html,
subject,
};
}

View File

@@ -0,0 +1,99 @@
import {
Body,
Head,
Html,
Link,
Preview,
Tailwind,
Text,
render
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import CommonFooter from '../components/common-footer';
import { EmailContent } from '../components/content';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
export async function renderNewJobsAvailableEmail({
language,
analysisResponseIds,
}: {
language?: string;
analysisResponseIds: number[];
}) {
const namespace = 'new-jobs-available-email';
const { t } = await initializeEmailI18n({
language,
namespace: [namespace, 'common'],
});
const previewText = t(`${namespace}:previewText`, {
nr: analysisResponseIds.length,
});
const subject = t(`${namespace}:subject`, {
nr: analysisResponseIds.length,
});
const html = await render(
<Html>
<Head>
<BodyStyle />
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body>
<EmailWrapper>
<EmailHeader>
<EmailHeading>{previewText}</EmailHeading>
</EmailHeader>
<EmailContent>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:hello`)}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:resultsReceivedForOrders`, {
nr: analysisResponseIds.length,
})}
</Text>
<Text className="text-[16px] leading-[24px] font-semibold text-[#242424]">
{t(`${namespace}:openOrdersHeading`, {
nr: analysisResponseIds.length,
})}
</Text>
<ul className="list-none text-[16px] leading-[24px]">
{analysisResponseIds.map((analysisResponseId, index) => (
<li>
<Link
key={analysisResponseId}
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisResponseId}`}
>
{t(`${namespace}:linkText`, { nr: index + 1 })}
</Link>
</li>
))}
</ul>
<Text>
{t(`${namespace}:ifLinksDisabled`)}{' '}
{`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/open-jobs`}
</Text>
<CommonFooter t={t} />
</EmailContent>
</EmailWrapper>
</Body>
</Tailwind>
</Html>,
);
return {
html,
subject,
};
}

View File

@@ -71,7 +71,7 @@ export async function renderOtpEmail(props: Props) {
<Section className="mb-[16px] mt-[16px] text-center">
<Button className={'w-full rounded bg-neutral-950 text-center'}>
<Text className="text-[16px] font-medium font-semibold leading-[16px] text-white">
<Text className="text-[16px] font-semibold leading-[16px] text-white">
{props.otp}
</Text>
</Button>

View File

@@ -9,12 +9,12 @@ import {
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import CommonFooter from '../components/common-footer';
import { EmailContent } from '../components/content';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
import CommonFooter from '../components/common-footer';
interface Props {
analysisPackageName: string;
@@ -31,7 +31,10 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) {
namespace: [namespace, 'common'],
});
const previewText = t(`${namespace}:previewText`);
const previewText = t(`${namespace}:previewText`, {
analysisPackageName: props.analysisPackageName,
});
const subject = t(`${namespace}:subject`, {
analysisPackageName: props.analysisPackageName,
});

View File

@@ -4,3 +4,6 @@ export * from './emails/otp.email';
export * from './emails/company-offer.email';
export * from './emails/synlab.email';
export * from './emails/doctor-summary-received.email';
export * from './emails/new-jobs-available.email';
export * from './emails/first-results-received.email';
export * from './emails/all-results-received.email';

View File

@@ -0,0 +1,8 @@
{
"previewText": "All analysis results have been received",
"subject": "All patient analysis results have been received",
"openOrdersHeading": "Review the results and prepare a summary:",
"linkText": "See results",
"ifLinksDisabled": "If the link does not work, you can see the results by copying this link into your browser.",
"hello": "Hello"
}

View File

@@ -0,0 +1,8 @@
{
"subject": "New Company Join Request",
"previewText": "The company {{companyName}} is requesting a quote",
"companyName": "Company Name:",
"contactPerson": "Contact Person:",
"email": "Email:",
"phone": "Phone:"
}

View File

@@ -0,0 +1,9 @@
{
"previewText": "First analysis responses received",
"subject": "New job - first analysis responses received",
"resultsReceivedForOrders": "New job available to claim",
"openOrdersHeading": "See here:",
"linkText": "See results",
"ifLinksDisabled": "If the link does not work, you can see available jobs by copying this link into your browser.",
"hello": "Hello,"
}

View File

@@ -0,0 +1,9 @@
{
"previewText": "New jobs available",
"subject": "Please write a summary",
"resultsReceivedForOrders": "Please review the results and write a summary.",
"openOrdersHeading": "See here:",
"linkText": "Open job {{nr}}",
"ifLinksDisabled": "If the links do not work, you can see available jobs by copying this link into your browser.",
"hello": "Hello,"
}

View File

@@ -0,0 +1,8 @@
{
"previewText": "Kõik analüüside vastused on saabunud",
"subject": "Patsiendi kõikide analüüside vastused on saabunud",
"openOrdersHeading": "Vaata tulemusi ja kirjuta kokkuvõte:",
"linkText": "Vaata tulemusi",
"ifLinksDisabled": "Kui link ei tööta, näed analüüsitulemusi sellelt aadressilt:",
"hello": "Tere"
}

View File

@@ -0,0 +1,9 @@
{
"previewText": "Saabusid esimesed analüüside vastused",
"subject": "Uus töö - saabusid esimesed analüüside vastused",
"resultsReceivedForOrders": "Patsiendile saabusid esimesed analüüside vastused.",
"openOrdersHeading": "Vaata siit:",
"linkText": "Vaata tulemusi",
"ifLinksDisabled": "Kui link ei tööta, näed analüüsitulemusi sellelt aadressilt:",
"hello": "Tere"
}

View File

@@ -0,0 +1,9 @@
{
"previewText": "Palun koosta kokkuvõte",
"subject": "Palun koosta kokkuvõte",
"resultsReceivedForOrders": "Palun vaata tulemused üle ja kirjuta kokkuvõte.",
"openOrdersHeading": "Vaata siit:",
"linkText": "Töö {{nr}}",
"ifLinksDisabled": "Kui lingid ei tööta, näed vabasid töid sellelt aadressilt:",
"hello": "Tere"
}

View File

@@ -0,0 +1,8 @@
{
"previewText": "All analysis results have been received",
"subject": "All patient analysis results have been received",
"openOrdersHeading": "Review the results and prepare a summary:",
"linkText": "See results",
"ifLinksDisabled": "If the link does not work, you can see the results by copying this link into your browser.",
"hello": "Hello"
}

View File

@@ -1,8 +1,8 @@
{
"footer": {
"lines1": "MedReport",
"lines2": "E-mail: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
"lines3": "Klienditugi: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
"lines2": "Электронная почта: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
"lines3": "Служба поддержки: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
"lines4": "<a href=\"https://www.medreport.ee\">www.medreport.ee</a>"
}
}

View File

@@ -1,8 +1,8 @@
{
"subject": "Uus ettevõtte liitumispäring",
"previewText": "Ettevõte {{companyName}} soovib pakkumist",
"companyName": "Ettevõtte nimi:",
"contactPerson": "Kontaktisik:",
"email": "E-mail:",
"phone": "Telefon:"
"subject": "Новый запрос на присоединение компании",
"previewText": "Компания {{companyName}} запрашивает предложение",
"companyName": "Название компании:",
"contactPerson": "Контактное лицо:",
"email": "Электронная почта:",
"phone": "Телефон:"
}

View File

@@ -1,8 +1,8 @@
{
"subject": "Saabus arsti kokkuvõtte tellimusele {{orderNr}}",
"previewText": "Arst on saatnud kokkuvõtte sinu analüüsitulemustele.",
"hello": "Tere, {{displayName}}",
"summaryReceivedForOrder": "Arst on koostanud selgitava kokkuvõtte sinu tellitud analüüsidele.",
"linkText": "Vaata kokkuvõtet",
"ifButtonDisabled": "Kui nupule vajutamine ei toimi, kopeeri see link oma brauserisse:"
"subject": "Получено заключение врача по заказу {{orderNr}}",
"previewText": "Врач отправил заключение по вашим результатам анализа.",
"hello": "Здравствуйте, {{displayName}}",
"summaryReceivedForOrder": "Врач подготовил пояснительное заключение по заказанным вами анализам.",
"linkText": "Посмотреть заключение",
"ifButtonDisabled": "Если кнопка не работает, скопируйте эту ссылку в ваш браузер:"
}

View File

@@ -0,0 +1,9 @@
{
"previewText": "First analysis responses received",
"subject": "New job - first analysis responses received",
"resultsReceivedForOrders": "New job available to claim",
"openOrdersHeading": "See here:",
"linkText": "See results",
"ifLinksDisabled": "If the link does not work, you can see the results by copying this link into your browser.",
"hello": "Hello,"
}

View File

@@ -0,0 +1,9 @@
{
"previewText": "New jobs available",
"subject": "Please write a summary",
"resultsReceivedForOrders": "Please review the results and write a summary.",
"openOrdersHeading": "See here:",
"linkText": "Open job {{nr}}",
"ifLinksDisabled": "If the links do not work, you can see available jobs by copying this link into your browser.",
"hello": "Hello,"
}

View File

@@ -1,12 +1,12 @@
{
"subject": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
"previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
"heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
"hello": "Tere {{personName}},",
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Synlab - {{partnerLocationName}}",
"lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>",
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
"lines5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.",
"lines6": "SYNLAB klienditoe telefon: <a href=\"tel:+37217123\">17123</a>"
"subject": "Ваш заказ Medreport подтвержден - {{analysisPackageName}}",
"previewText": "Ваш заказ Medreport подтвержден - {{analysisPackageName}}",
"heading": "Ваш заказ Medreport подтвержден - {{analysisPackageName}}",
"hello": "Здравствуйте, {{personName}},",
"lines1": "Направление на исследование {{analysisPackageName}} было отправлено в лабораторию в цифровом виде. Пожалуйста, сдайте анализы: Synlab - {{partnerLocationName}}",
"lines2": "<i>Если вы не можете посетить выбранный пункт сдачи анализов, вы можете обратиться в удобный для вас пункт - <a href=\"https://medreport.ee/et/verevotupunktid\">посмотреть адреса и часы работы</a>.</i>",
"lines3": "Рекомендуется сдавать анализы утром (до 12:00) натощак (можно пить воду).",
"lines4": "В пункте сдачи анализов выберите в системе очереди: <strong>направления</strong> -> <strong>направление от специалиста</strong>.",
"lines5": "Если у вас возникнут дополнительные вопросы, смело свяжитесь с нами.",
"lines6": "Телефон службы поддержки SYNLAB: <a href=\"tel:+37217123\">17123</a>"
}

View File

@@ -5,12 +5,16 @@ import { useSupabase } from '@kit/supabase/hooks/use-supabase';
type UpdateData = Database['medreport']['Tables']['accounts']['Update'];
export function useUpdateAccountData(accountId: string) {
export function useUpdateAccountData(accountId?: string) {
const client = useSupabase();
const mutationKey = ['account:data', accountId];
const mutationFn = async (data: UpdateData) => {
if (!accountId) {
return null;
}
const response = await client
.schema('medreport')
.from('accounts')

View File

@@ -2,7 +2,11 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import { UserAnalysis } from '../types/accounts';
import {
AnalysisResultDetails,
UserAnalysis,
UserAnalysisResponse,
} from '../types/accounts';
export type AccountWithParams =
Database['medreport']['Tables']['accounts']['Row'] & {
@@ -184,7 +188,49 @@ class AccountsApi {
return response.data?.customer_id;
}
async getUserAnalysis(): Promise<UserAnalysis | null> {
async getUserAnalysis(
analysisOrderId: number,
): Promise<AnalysisResultDetails | null> {
const authUser = await this.client.auth.getUser();
const { data, error: userError } = authUser;
if (userError) {
console.error('Failed to get user', userError);
throw userError;
}
const { user } = data;
const { data: analysisResponse } = await this.client
.schema('medreport')
.from('analysis_responses')
.select(
`*,
elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time),
order:analysis_order_id(medusa_order_id, status, created_at),
summary:analysis_order_id(doctor_analysis_feedback(*))`,
)
.eq('user_id', user.id)
.eq('analysis_order_id', analysisOrderId)
.throwOnError();
const responseWithElements = analysisResponse?.[0];
if (!responseWithElements) {
return null;
}
const feedback = responseWithElements.summary.doctor_analysis_feedback?.[0];
return {
...responseWithElements,
summary:
feedback?.status === 'COMPLETED'
? responseWithElements.summary.doctor_analysis_feedback?.[0]
: null,
};
}
async getUserAnalyses(): Promise<UserAnalysis | null> {
const authUser = await this.client.auth.getUser();
const { data, error: userError } = authUser;

View File

@@ -1,3 +1,5 @@
import * as z from 'zod';
import { Database } from '@kit/supabase/database';
export type UserAnalysisElement =
@@ -15,3 +17,51 @@ export enum ApplicationRoleEnum {
Doctor = 'doctor',
SuperAdmin = 'super_admin',
}
export const ElementSchema = z.object({
unit: z.string(),
norm_lower: z.number(),
norm_upper: z.number(),
norm_status: z.number(),
analysis_name: z.string(),
response_time: z.string(),
response_value: z.number(),
norm_lower_included: z.boolean(),
norm_upper_included: z.boolean(),
});
export type Element = z.infer<typeof ElementSchema>;
export const OrderSchema = z.object({
status: z.string(),
medusa_order_id: z.string(),
created_at: z.coerce.date(),
});
export type Order = z.infer<typeof OrderSchema>;
export const SummarySchema = z.object({
id: z.number(),
value: z.string(),
status: z.string(),
user_id: z.string(),
created_at: z.coerce.date(),
created_by: z.string(),
updated_at: z.coerce.date().nullable(),
updated_by: z.string(),
doctor_user_id: z.string().nullable(),
analysis_order_id: z.number(),
});
export type Summary = z.infer<typeof SummarySchema>;
export const AnalysisResultDetailsSchema = z.object({
id: z.number(),
analysis_order_id: z.number(),
order_number: z.string(),
order_status: z.string(),
user_id: z.string(),
created_at: z.coerce.date(),
updated_at: z.coerce.date().nullable(),
elements: z.array(ElementSchema),
order: OrderSchema,
summary: SummarySchema.nullable(),
});
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;

View File

@@ -7,7 +7,7 @@ export function AuthLayoutShell({
return (
<div
className={
'flex h-screen flex-col items-center justify-center' +
'sm:py-auto flex flex-col items-center justify-center py-6' +
' bg-background lg:bg-muted/30 gap-y-10 lg:gap-y-8' +
' animate-in fade-in slide-in-from-top-16 zoom-in-95 duration-1000'
}

View File

@@ -41,6 +41,7 @@ export const PatientSchema = z.object({
email: z.string().nullable(),
height: z.number().optional().nullable(),
weight: z.number().optional().nullable(),
preferred_locale: z.string().nullable(),
});
export type Patient = z.infer<typeof PatientSchema>;

View File

@@ -80,6 +80,7 @@ export const AccountSchema = z.object({
last_name: z.string().nullable(),
id: z.string(),
primary_owner_user_id: z.string(),
preferred_locale: z.string().nullable(),
});
export type Account = z.infer<typeof AccountSchema>;

View File

@@ -2,10 +2,11 @@ import 'server-only';
import { isBefore } from 'date-fns';
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
import { getFullName } from '@kit/shared/utils';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { sendDoctorSummaryCompletedEmail } from '../../../../../../../lib/services/mailer.service';
import { sendEmailFromTemplate } from '../../../../../../../lib/services/mailer.service';
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
import {
AnalysisResponseBase,
@@ -54,7 +55,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
supabase
.schema('medreport')
.from('accounts')
.select('name, last_name, id, primary_owner_user_id')
.select('name, last_name, id, primary_owner_user_id, preferred_locale')
.in('primary_owner_user_id', userIds),
]);
@@ -67,7 +68,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
? await supabase
.schema('medreport')
.from('accounts')
.select('name, last_name, id, primary_owner_user_id')
.select('name, last_name, id, primary_owner_user_id, preferred_locale')
.in('primary_owner_user_id', doctorUserIds)
: { data: [] };
@@ -408,7 +409,7 @@ export async function getAnalysisResultsForDoctor(
.schema('medreport')
.from('accounts')
.select(
`primary_owner_user_id, id, name, last_name, personal_code, phone, email,
`primary_owner_user_id, id, name, last_name, personal_code, phone, email, preferred_locale,
account_params(height,weight)`,
)
.eq('is_personal_account', true)
@@ -472,6 +473,7 @@ export async function getAnalysisResultsForDoctor(
personal_code,
phone,
account_params,
preferred_locale,
} = accountWithParams[0];
const analysisResponseElementsWithPreviousData = [];
@@ -503,6 +505,7 @@ export async function getAnalysisResultsForDoctor(
},
doctorFeedback: doctorFeedback?.[0],
patient: {
preferred_locale,
userId: primary_owner_user_id,
accountId,
firstName: name,
@@ -638,7 +641,7 @@ export async function submitFeedback(
}
if (status === 'COMPLETED') {
const [{ data: recipient }, { data: medusaOrderIds }] = await Promise.all([
const [{ data: recipient }, { data: analysisOrder }] = await Promise.all([
supabase
.schema('medreport')
.from('accounts')
@@ -653,24 +656,33 @@ export async function submitFeedback(
.eq('id', analysisOrderId)
.limit(1)
.throwOnError(),
supabase
.schema('medreport')
.from('analysis_orders')
.update({ status: 'COMPLETED' })
.eq('id', analysisOrderId)
.throwOnError(),
]);
if (!recipient?.[0]?.email) {
throw new Error('Could not find user email.');
}
if (!medusaOrderIds?.[0]?.id) {
if (!analysisOrder?.[0]?.id) {
throw new Error('Could not retrieve order.');
}
const { preferred_locale, name, last_name, email } = recipient[0];
await sendDoctorSummaryCompletedEmail(
preferred_locale ?? 'et',
getFullName(name, last_name),
await sendEmailFromTemplate(
renderDoctorSummaryReceivedEmail,
{
language: preferred_locale ?? 'et',
recipientName: getFullName(name, last_name),
orderNr: analysisOrder?.[0]?.medusa_order_id ?? '',
analysisOrderId: analysisOrder[0].id,
},
email,
medusaOrderIds?.[0]?.medusa_order_id ?? '',
medusaOrderIds[0].id,
);
}

View File

@@ -1,6 +1,8 @@
import { Trans } from '@kit/ui/trans';
import SelectAnalysisPackage, { AnalysisPackageWithVariant } from './select-analysis-package';
import SelectAnalysisPackage, {
AnalysisPackageWithVariant,
} from './select-analysis-package';
export default function SelectAnalysisPackages({
analysisPackages,
@@ -10,11 +12,16 @@ export default function SelectAnalysisPackages({
countryCode: string;
}) {
return (
<div className="grid grid-cols-3 gap-6">
{analysisPackages.length > 0 ? analysisPackages.map(
(analysisPackage) => (
<SelectAnalysisPackage key={analysisPackage.title} analysisPackage={analysisPackage} countryCode={countryCode} />
)) : (
<div className="grid gap-6 sm:grid-cols-3">
{analysisPackages.length > 0 ? (
analysisPackages.map((analysisPackage) => (
<SelectAnalysisPackage
key={analysisPackage.title}
analysisPackage={analysisPackage}
countryCode={countryCode}
/>
))
) : (
<h4>
<Trans i18nKey="order-analysis-package:noPackagesAvailable" />
</h4>

View File

@@ -35,12 +35,6 @@ const routes = [
Icon: <ShoppingCart className={iconClasses} />,
end: true,
},
{
label: 'common:routes.analysisResults',
path: pathsConfig.app.analysisResults,
Icon: <TestTube2 className={iconClasses} />,
end: true,
},
{
label: 'common:routes.orderAnalysisPackage',
path: pathsConfig.app.orderAnalysisPackage,

View File

@@ -1 +1,2 @@
export * from './use-csrf-token';
export * from './use-current-locale-language-names';

View File

@@ -0,0 +1,17 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
function useLanguageName(currentLanguage: string) {
return useMemo(() => {
return new Intl.DisplayNames([currentLanguage], {
type: 'language',
});
}, [currentLanguage]);
}
export function useCurrentLocaleLanguageNames() {
const { i18n } = useTranslation();
const { language: currentLanguage } = i18n;
return useLanguageName(currentLanguage);
}

View File

@@ -40,7 +40,7 @@ export function LanguageSelector({
}, [currentLanguage]);
const userId = user?.id;
const updateAccountMutation = useUpdateAccountData(userId!);
const updateAccountMutation = useUpdateAccountData(userId);
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
const updateLanguagePreference = async (
@@ -52,6 +52,10 @@ export function LanguageSelector({
onChange(locale);
}
if (!userId) {
return i18n.changeLanguage(locale);
}
const promise = updateAccountMutation
.mutateAsync({
preferred_locale: locale,

View File

@@ -1,5 +1,7 @@
import { cn } from '../../lib/utils';
import { LanguageSelector } from '@kit/ui/language-selector';
import { cn } from '../../lib/utils';
interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
logo?: React.ReactNode;
navigation?: React.ReactNode;
@@ -22,11 +24,16 @@ export const Header: React.FC<HeaderProps> = function ({
{...props}
>
<div className="container">
<div className="grid h-14 grid-cols-3 items-center">
<div className={'mx-auto md:mx-0'}>{logo}</div>
<div className="order-first md:order-none">{navigation}</div>
<div className="2xs:h-14 2xs:grid-cols-3 grid h-24 w-full items-center">
<div className={'mx-auto flex'}>{logo}</div>
<div className="2xs:order-none order-first">{navigation}</div>
<div className="flex items-center justify-end gap-x-2"><div className="max-w-[100px]"><LanguageSelector /></div>{actions}</div>
<div className="2xs:justify-end 2xs:gap-x-2 flex items-center justify-evenly">
<div className="max-w-[100px]">
<LanguageSelector />
</div>
{actions}
</div>
</div>
</div>
</div>

View File

@@ -42,7 +42,7 @@ function PageWithSidebar(props: PageProps) {
>
{MobileNavigation}
<div className={'bg-background flex flex-1 flex-col px-4 lg:px-0 pb-8'}>
<div className={'bg-background flex flex-1 flex-col px-4 pb-8 lg:px-0'}>
{Children}
</div>
</div>
@@ -58,7 +58,7 @@ export function PageMobileNavigation(
return (
<div
className={cn(
'flex w-full items-center border-b px-4 py-2 lg:hidden lg:px-0',
'flex w-full items-center px-4 py-2 lg:hidden lg:px-0',
props.className,
)}
>

View File

@@ -128,5 +128,6 @@
"updateRoleLoading": "Updating role...",
"updatePreferredLocaleSuccess": "Language preference updated",
"updatePreferredLocaleError": "Language preference update failed",
"updatePreferredLocaleLoading": "Updating language preference..."
"updatePreferredLocaleLoading": "Updating language preference...",
"doctorAnalysisSummary": "Doctor's summary"
}

View File

@@ -12,5 +12,6 @@
"normal": "Normal range"
}
},
"orderTitle": "Order number {{orderNumber}}"
"orderTitle": "Order number {{orderNumber}}",
"view": "View results"
}

View File

@@ -116,5 +116,6 @@
"confirm": "Confirm",
"previous": "Previous",
"next": "Next",
"invalidDataError": "Invalid data submitted"
"invalidDataError": "Invalid data submitted",
"language": "Language"
}

View File

@@ -17,6 +17,7 @@
"assignedTo": "Doctor",
"resultsStatus": "Analysis results",
"waitingForNr": "Waiting for {{nr}}",
"language": "Preferred language",
"responsesReceived": "Results complete"
},
"otherPatients": "Other patients",

View File

@@ -151,5 +151,6 @@
"updateRoleLoading": "Rolli uuendatakse...",
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse..."
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse...",
"doctorAnalysisSummary": "Arsti kokkuvõte analüüsitulemuste kohta"
}

View File

@@ -12,5 +12,6 @@
"normal": "Normaalne vahemik"
}
},
"orderTitle": "Tellimus {{orderNumber}}"
"orderTitle": "Tellimus {{orderNumber}}",
"view": "Vaata tulemusi"
}

View File

@@ -136,5 +136,6 @@
"confirm": "Kinnita",
"previous": "Eelmine",
"next": "Järgmine",
"invalidDataError": "Vigased andmed"
"invalidDataError": "Vigased andmed",
"language": "Keel"
}

View File

@@ -17,6 +17,7 @@
"assignedTo": "Arst",
"resultsStatus": "Analüüsitulemused",
"waitingForNr": "Ootel {{nr}}",
"language": "Patsiendi keel",
"responsesReceived": "Tulemused koos"
},
"otherPatients": "Muud patsiendid",

View File

@@ -1,132 +1,155 @@
{
"accountTabLabel": "Account Settings",
"accountTabDescription": "Manage your account settings",
"homePage": "Home",
"billingTab": "Billing",
"settingsTab": "Settings",
"multiFactorAuth": "Multi-Factor Authentication",
"multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account",
"updateProfileSuccess": "Profile successfully updated",
"updateProfileError": "Encountered an error. Please try again",
"updatePasswordSuccess": "Password update request successful",
"updatePasswordSuccessMessage": "Your password has been successfully updated!",
"updatePasswordError": "Encountered an error. Please try again",
"updatePasswordLoading": "Updating password...",
"updateProfileLoading": "Updating profile...",
"name": "Your Name",
"nameDescription": "Update your name to be displayed on your profile",
"emailLabel": "Email Address",
"accountImage": "Your Profile Picture",
"accountImageDescription": "Please choose a photo to upload as your profile picture.",
"profilePictureHeading": "Upload a Profile Picture",
"profilePictureSubheading": "Choose a photo to upload as your profile picture.",
"updateProfileSubmitLabel": "Update Profile",
"updatePasswordCardTitle": "Update your Password",
"updatePasswordCardDescription": "Update your password to keep your account secure.",
"currentPassword": "Current Password",
"newPassword": "New Password",
"repeatPassword": "Repeat New Password",
"repeatPasswordDescription": "Please repeat your new password to confirm it",
"yourPassword": "Your Password",
"updatePasswordSubmitLabel": "Update Password",
"updateEmailCardTitle": "Update your Email",
"updateEmailCardDescription": "Update your email address you use to login to your account",
"newEmail": "Your New Email",
"repeatEmail": "Repeat Email",
"updateEmailSubmitLabel": "Update Email Address",
"updateEmailSuccess": "Email update request successful",
"updateEmailSuccessMessage": "We sent you an email to confirm your new email address. Please check your inbox and click on the link to confirm your new email address.",
"updateEmailLoading": "Updating your email...",
"updateEmailError": "Email not updated. Please try again",
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
"emailNotMatching": "Emails do not match. Make sure you're using the correct email",
"passwordNotChanged": "Your password has not changed",
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
"cannotUpdatePassword": "You cannot update your password because your account is not linked to any.",
"setupMfaButtonLabel": "Setup a new Factor",
"multiFactorSetupErrorHeading": "Setup Failed",
"multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.",
"multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication",
"multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.",
"factorNameLabel": "A memorable name to identify this factor",
"factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14",
"factorNameSubmitLabel": "Set factor name",
"unenrollTooltip": "Unenroll this factor",
"unenrollingFactor": "Unenrolling factor...",
"unenrollFactorSuccess": "Factor successfully unenrolled",
"unenrollFactorError": "Unenrolling factor failed",
"factorsListError": "Error loading factors list",
"factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.",
"factorName": "Factor Name",
"factorType": "Type",
"factorStatus": "Status",
"mfaEnabledSuccessTitle": "Multi-Factor authentication is enabled",
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
"verificationCode": "Verification Code",
"addEmailAddress": "Add Email address",
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above",
"loadingFactors": "Loading factors...",
"enableMfaFactor": "Enable Factor",
"disableMfaFactor": "Disable Factor",
"qrCodeErrorHeading": "QR Code Error",
"qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code",
"multiFactorSetupSuccess": "Factor successfully enrolled",
"submitVerificationCode": "Submit Verification Code",
"mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled",
"verifyingCode": "Verifying code...",
"invalidVerificationCodeHeading": "Invalid Verification Code",
"invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.",
"unenrollFactorModalHeading": "Unenroll Factor",
"unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
"unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
"unenrollFactorModalButtonLabel": "Yes, unenroll factor",
"selectFactor": "Choose a factor to verify your identity",
"disableMfa": "Disable Multi-Factor Authentication",
"disableMfaButtonLabel": "Disable MFA",
"confirmDisableMfaButtonLabel": "Yes, disable MFA",
"disablingMfa": "Disabling Multi-Factor Authentication. Please wait...",
"disableMfaSuccess": "Multi-Factor Authentication successfully disabled",
"disableMfaError": "Sorry, we encountered an error. MFA has not been disabled.",
"sendingEmailVerificationLink": "Sending Email...",
"sendEmailVerificationLinkSuccess": "Verification link successfully sent",
"sendEmailVerificationLinkError": "Sorry, we weren't able to send you the email",
"sendVerificationLinkSubmitLabel": "Send Verification Link",
"sendVerificationLinkSuccessLabel": "Email sent! Check your Inbox",
"verifyEmailAlertHeading": "Please verify your email to enable MFA",
"verificationLinkAlertDescription": "Your email is not yet verified. Please verify your email to be able to set up Multi-Factor Authentication.",
"authFactorName": "Factor Name (optional)",
"authFactorNameHint": "Assign a name that helps you remember the phone number used",
"loadingUser": "Loading user details. Please wait...",
"linkPhoneNumber": "Link Phone Number",
"dangerZone": "Danger Zone",
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.",
"deleteAccount": "Delete your Account",
"deletingAccount": "Deleting account. Please wait...",
"deleteAccountDescription": "This will delete your account and the accounts you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone.",
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
"deleteAccountErrorHeading": "Sorry, we couldn't delete your account",
"needsReauthentication": "Reauthentication Required",
"needsReauthenticationDescription": "You need to reauthenticate to change your password. Please sign out and sign in again to change your password.",
"language": "Language",
"languageDescription": "Choose your preferred language",
"noTeamsYet": "You don't have any teams yet.",
"createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account",
"updateConsentSuccess": "Consent successfully updated",
"updateConsentError": "Encountered an error. Please try again",
"updateConsentLoading": "Updating consent...",
"accountTabLabel": "Настройки аккаунта",
"accountTabDescription": "Управляйте настройками вашего аккаунта",
"homePage": "Главная",
"billingTab": "Оплата",
"settingsTab": "Настройки",
"multiFactorAuth": "Многофакторная аутентификация",
"multiFactorAuthDescription": "Настройте метод многофакторной аутентификации для дополнительной защиты вашего аккаунта",
"updateProfileSuccess": "Профиль успешно обновлен",
"updateProfileError": "Произошла ошибка. Пожалуйста, попробуйте снова",
"updatePasswordSuccess": "Запрос на обновление пароля выполнен успешно",
"updatePasswordSuccessMessage": "Ваш пароль был успешно обновлен!",
"updatePasswordError": "Произошла ошибка. Пожалуйста, попробуйте снова",
"updatePasswordLoading": "Обновление пароля...",
"updateProfileLoading": "Обновление профиля...",
"name": "Ваше имя",
"nameDescription": "Обновите ваше имя, которое будет отображаться в профиле",
"emailLabel": "Адрес электронной почты",
"accountImage": "Ваша фотография профиля",
"accountImageDescription": "Выберите фото для загрузки в качестве изображения профиля.",
"profilePictureHeading": "Загрузить фотографию профиля",
"profilePictureSubheading": "Выберите фото для загрузки в качестве изображения профиля.",
"updateProfileSubmitLabel": "Обновить профиль",
"updatePasswordCardTitle": "Обновите ваш пароль",
"updatePasswordCardDescription": "Обновите пароль, чтобы сохранить ваш аккаунт в безопасности.",
"currentPassword": "Текущий пароль",
"newPassword": "Новый пароль",
"repeatPassword": "Повторите новый пароль",
"repeatPasswordDescription": "Пожалуйста, повторите новый пароль для подтверждения",
"yourPassword": "Ваш пароль",
"updatePasswordSubmitLabel": "Обновить пароль",
"updateEmailCardTitle": "Обновите вашу почту",
"updateEmailCardDescription": "Обновите адрес электронной почты, который вы используете для входа в аккаунт",
"newEmail": "Новый адрес электронной почты",
"repeatEmail": "Повторите адрес электронной почты",
"updateEmailSubmitLabel": "Обновить адрес электронной почты",
"updateEmailSuccess": "Запрос на обновление почты выполнен успешно",
"updateEmailSuccessMessage": "Мы отправили вам письмо для подтверждения нового адреса. Пожалуйста, проверьте почту и перейдите по ссылке для подтверждения.",
"updateEmailLoading": "Обновление почты...",
"updateEmailError": "Почта не обновлена. Пожалуйста, попробуйте снова",
"passwordNotMatching": "Пароли не совпадают. Убедитесь, что вы используете правильный пароль",
"emailNotMatching": "Адреса электронной почты не совпадают. Убедитесь, что вы используете правильный адрес",
"passwordNotChanged": "Ваш пароль не был изменен",
"emailsNotMatching": "Адреса электронной почты не совпадают. Убедитесь, что вы используете правильный адрес",
"cannotUpdatePassword": "Вы не можете обновить пароль, так как ваш аккаунт не связан ни с одним.",
"setupMfaButtonLabel": "Настроить новый фактор",
"multiFactorSetupErrorHeading": "Сбой настройки",
"multiFactorSetupErrorDescription": "Извините, произошла ошибка при настройке фактора. Пожалуйста, попробуйте снова.",
"multiFactorAuthHeading": "Защитите ваш аккаунт с помощью многофакторной аутентификации",
"multiFactorModalHeading": "Используйте приложение-аутентификатор для сканирования QR-кода ниже, затем введите сгенерированный код.",
"factorNameLabel": "Запоминающееся имя для идентификации этого фактора",
"factorNameHint": "Используйте простое для запоминания имя, чтобы легко идентифицировать этот фактор в будущем. Пример: iPhone 14",
"factorNameSubmitLabel": "Задать имя фактора",
"unenrollTooltip": "Удалить фактор",
"unenrollingFactor": "Удаление фактора...",
"unenrollFactorSuccess": "Фактор успешно удален",
"unenrollFactorError": "Не удалось удалить фактор",
"factorsListError": "Ошибка загрузки списка факторов",
"factorsListErrorDescription": "Извините, не удалось загрузить список факторов. Пожалуйста, попробуйте снова.",
"factorName": "Имя фактора",
"factorType": "Тип",
"factorStatus": "Статус",
"mfaEnabledSuccessTitle": "Многофакторная аутентификация включена",
"mfaEnabledSuccessDescription": "Поздравляем! Вы успешно подключили многофакторную аутентификацию. Теперь вы сможете входить в аккаунт с помощью комбинации пароля и кода подтверждения, отправленного на ваш номер телефона.",
"verificationCode": "Код подтверждения",
"addEmailAddress": "Добавить адрес электронной почты",
"verifyActivationCodeDescription": "Введите 6-значный код, сгенерированный вашим приложением-аутентификатором, в поле выше",
"loadingFactors": "Загрузка факторов...",
"enableMfaFactor": "Включить фактор",
"disableMfaFactor": "Отключить фактор",
"qrCodeErrorHeading": "Ошибка QR-кода",
"qrCodeErrorDescription": "Извините, не удалось сгенерировать QR-код",
"multiFactorSetupSuccess": "Фактор успешно подключен",
"submitVerificationCode": "Отправить код подтверждения",
"mfaEnabledSuccessAlert": "Многофакторная аутентификация включена",
"verifyingCode": "Проверка кода...",
"invalidVerificationCodeHeading": "Неверный код подтверждения",
"invalidVerificationCodeDescription": "Введенный вами код неверен. Пожалуйста, попробуйте снова.",
"unenrollFactorModalHeading": "Удаление фактора",
"unenrollFactorModalDescription": "Вы собираетесь удалить этот фактор. Вы больше не сможете использовать его для входа в аккаунт.",
"unenrollFactorModalBody": "Вы собираетесь удалить этот фактор. Вы больше не сможете использовать его для входа в аккаунт.",
"unenrollFactorModalButtonLabel": "Да, удалить фактор",
"selectFactor": "Выберите фактор для подтверждения личности",
"disableMfa": "Отключить многофакторную аутентификацию",
"disableMfaButtonLabel": "Отключить MFA",
"confirmDisableMfaButtonLabel": "Да, отключить MFA",
"disablingMfa": "Отключение многофакторной аутентификации. Пожалуйста, подождите...",
"disableMfaSuccess": "Многофакторная аутентификация успешно отключена",
"disableMfaError": "Извините, произошла ошибка. MFA не была отключена.",
"sendingEmailVerificationLink": "Отправка письма...",
"sendEmailVerificationLinkSuccess": "Ссылка для подтверждения успешно отправлена",
"sendEmailVerificationLinkError": "Извините, не удалось отправить письмо",
"sendVerificationLinkSubmitLabel": "Отправить ссылку для подтверждения",
"sendVerificationLinkSuccessLabel": "Письмо отправлено! Проверьте почту",
"verifyEmailAlertHeading": "Пожалуйста, подтвердите вашу почту, чтобы включить MFA",
"verificationLinkAlertDescription": "Ваша почта еще не подтверждена. Пожалуйста, подтвердите ее, чтобы настроить многофакторную аутентификацию.",
"authFactorName": "Имя фактора (необязательно)",
"authFactorNameHint": "Присвойте имя, которое поможет вам запомнить номер телефона, используемый для входа",
"loadingUser": "Загрузка данных пользователя. Пожалуйста, подождите...",
"linkPhoneNumber": "Привязать номер телефона",
"dangerZone": "Опасная зона",
"dangerZoneDescription": "Некоторые действия нельзя отменить. Будьте осторожны.",
"deleteAccount": "Удалить аккаунт",
"deletingAccount": "Удаление аккаунта. Пожалуйста, подождите...",
"deleteAccountDescription": "Это удалит ваш аккаунт и связанные с ним учетные записи. Также мы немедленно отменим все активные подписки. Это действие нельзя отменить.",
"deleteProfileConfirmationInputLabel": "Введите DELETE для подтверждения",
"deleteAccountErrorHeading": "Извините, не удалось удалить аккаунт",
"needsReauthentication": "Требуется повторная аутентификация",
"needsReauthenticationDescription": "Необходимо повторно войти в систему, чтобы изменить пароль. Пожалуйста, выйдите и войдите снова.",
"language": "Язык",
"languageDescription": "Выберите предпочитаемый язык",
"noTeamsYet": "У вас пока нет команд.",
"createTeam": "Создайте команду, чтобы начать.",
"createTeamButtonLabel": "Создать команду",
"createCompanyAccount": "Создать аккаунт компании",
"requestCompanyAccount": {
"title": "Данные компании",
"description": "Чтобы получить предложение, пожалуйста, введите данные компании, с которой вы планируете использовать MedReport.",
"button": "Запросить предложение",
"successTitle": "Запрос успешно отправлен!",
"successDescription": "Мы ответим вам при первой возможности",
"successButton": "Вернуться на главную"
},
"updateAccount": {
"title": "Личные данные",
"description": "Пожалуйста, введите свои личные данные для продолжения",
"button": "Продолжить",
"userConsentLabel": "Я согласен на использование моих персональных данных на платформе",
"userConsentUrlTitle": "Посмотреть политику обработки персональных данных"
},
"consentModal": {
"title": "Прежде чем начать",
"description": "Вы даете согласие на использование ваших медицинских данных в анонимной форме для статистики работодателя? Данные будут обезличены и помогут компании лучше поддерживать здоровье сотрудников.",
"reject": "Не даю согласие",
"accept": "Да, даю согласие"
},
"updateConsentSuccess": "Согласия обновлены",
"updateConsentError": "Что-то пошло не так. Пожалуйста, попробуйте снова",
"updateConsentLoading": "Обновление согласий...",
"consentToAnonymizedCompanyData": {
"label": "Consent to be included in employer statistics",
"description": "Consent to be included in anonymized company statistics"
"label": "Согласен участвовать в статистике работодателя",
"description": "Я согласен на использование моих медицинских данных в анонимной форме для статистики работодателя"
},
"analysisResults": {
"pageTitle": "My analysis results"
"membershipConfirmation": {
"successTitle": "Здравствуйте, {{firstName}} {{lastName}}",
"successDescription": "Ваш медицинский аккаунт активирован и готов к использованию!",
"successButton": "Продолжить"
},
"updateRoleSuccess": "Role updated",
"updateRoleError": "Something went wrong, please try again",
"updateRoleLoading": "Updating role...",
"updatePreferredLocaleSuccess": "Language preference updated",
"updatePreferredLocaleError": "Language preference update failed",
"updatePreferredLocaleLoading": "Updating language preference..."
"updateRoleSuccess": "Роль обновлена",
"updateRoleError": "Что-то пошло не так. Пожалуйста, попробуйте снова",
"updateRoleLoading": "Обновление роли...",
"updatePreferredLocaleSuccess": "Предпочитаемый язык обновлен",
"updatePreferredLocaleError": "Не удалось обновить предпочитаемый язык",
"updatePreferredLocaleLoading": "Обновление предпочитаемого языка..."
}

View File

@@ -0,0 +1,16 @@
{
"pageTitle": "Мои результаты анализов",
"description": "Все результаты анализов появляются в течение 1-3 рабочих дней после их сдачи.",
"descriptionEmpty": "Если вы уже сдали анализы, то вскоре здесь появятся ваши результаты.",
"orderNewAnalysis": "Заказать новые анализы",
"waitingForResults": "Ожидание результатов",
"noAnalysisElements": "Анализы еще не заказаны",
"noAnalysisOrders": "Пока нет заказов на анализы",
"analysisDate": "Дата результата анализа",
"results": {
"range": {
"normal": "Нормальный диапазон"
}
},
"orderTitle": "Заказ {{orderNumber}}"
}

View File

@@ -1,90 +1,90 @@
{
"signUpHeading": "Create an account",
"signUp": "Sign Up",
"signUpSubheading": "Fill the form below to create an account.",
"signInHeading": "Sign in to your account",
"signInSubheading": "Welcome back! Please enter your details",
"signIn": "Sign In",
"getStarted": "Get started",
"updatePassword": "Update Password",
"signOut": "Sign out",
"signingIn": "Signing in...",
"signingUp": "Signing up...",
"doNotHaveAccountYet": "Do not have an account yet?",
"alreadyHaveAnAccount": "Already have an account?",
"signUpToAcceptInvite": "Please sign in/up to accept the invite",
"clickToAcceptAs": "Click the button below to accept the invite with as <b>{{email}}</b>",
"acceptInvite": "Accept invite",
"acceptingInvite": "Accepting Invite...",
"acceptInviteSuccess": "Invite successfully accepted",
"acceptInviteError": "Error encountered while accepting invite",
"acceptInviteWithDifferentAccount": "Want to accept the invite with a different account?",
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead",
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead",
"signInWithProvider": "Sign in with {{provider}}",
"signInWithPhoneNumber": "Sign in with Phone Number",
"signInWithEmail": "Sign in with Email",
"signUpWithEmail": "Sign up with Email",
"passwordHint": "Ensure it's at least 8 characters",
"repeatPasswordHint": "Type your password again",
"repeatPassword": "Repeat password",
"passwordForgottenQuestion": "Password forgotten?",
"passwordResetLabel": "Reset Password",
"passwordResetSubheading": "Enter your email address below. You will receive a link to reset your password.",
"passwordResetSuccessMessage": "Check your Inbox! We emailed you a link for resetting your Password.",
"passwordRecoveredQuestion": "Password recovered?",
"passwordLengthError": "Please provide a password with at least 6 characters",
"sendEmailLink": "Send Email Link",
"sendingEmailLink": "Sending Email Link...",
"sendLinkSuccessDescription": "Check your email, we just sent you a link. Follow the link to sign in.",
"sendLinkSuccess": "We sent you a link by email",
"sendLinkSuccessToast": "Link successfully sent",
"getNewLink": "Get a new link",
"verifyCodeHeading": "Verify your account",
"verificationCode": "Verification Code",
"verificationCodeHint": "Enter the code we sent you by SMS",
"verificationCodeSubmitButtonLabel": "Submit Verification Code",
"sendingMfaCode": "Sending Verification Code...",
"verifyingMfaCode": "Verifying code...",
"sendMfaCodeError": "Sorry, we couldn't send you a verification code",
"verifyMfaCodeSuccess": "Code verified! Signing you in...",
"verifyMfaCodeError": "Ops! It looks like the code is not correct",
"reauthenticate": "Reauthenticate",
"reauthenticateDescription": "For security reasons, we need you to re-authenticate",
"errorAlertHeading": "Sorry, we could not authenticate you",
"emailConfirmationAlertHeading": "We sent you a confirmation email.",
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.",
"resendLink": "Resend link",
"resendLinkSuccessDescription": "We sent you a new link to your email! Follow the link to sign in.",
"resendLinkSuccess": "Check your email!",
"authenticationErrorAlertHeading": "Authentication Error",
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.",
"sendEmailCode": "Get code to your Email",
"sendingEmailCode": "Sending code...",
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
"signUpHeading": "Создать аккаунт",
"signUp": "Зарегистрироваться",
"signUpSubheading": "Заполните форму ниже, чтобы создать аккаунт.",
"signInHeading": "Войдите в свой аккаунт",
"signInSubheading": "С возвращением! Пожалуйста, введите свои данные",
"signIn": "Войти",
"getStarted": "Начать",
"updatePassword": "Обновить пароль",
"signOut": "Выйти",
"signingIn": "Вход...",
"signingUp": "Регистрация...",
"doNotHaveAccountYet": "Еще нет аккаунта?",
"alreadyHaveAnAccount": "Уже есть аккаунт?",
"signUpToAcceptInvite": "Пожалуйста, войдите или зарегистрируйтесь, чтобы принять приглашение",
"clickToAcceptAs": "Нажмите кнопку ниже, чтобы принять приглашение как <b>{{email}}</b>",
"acceptInvite": "Принять приглашение",
"acceptingInvite": "Принятие приглашения...",
"acceptInviteSuccess": "Приглашение успешно принято",
"acceptInviteError": "Произошла ошибка при принятии приглашения",
"acceptInviteWithDifferentAccount": "Хотите принять приглашение с другим аккаунтом?",
"alreadyHaveAccountStatement": "У меня уже есть аккаунт, я хочу войти",
"doNotHaveAccountStatement": "У меня нет аккаунта, я хочу зарегистрироваться",
"signInWithProvider": "Войти через {{provider}}",
"signInWithPhoneNumber": "Войти по номеру телефона",
"signInWithEmail": "Войти по Email",
"signUpWithEmail": "Зарегистрироваться по Email",
"passwordHint": "Убедитесь, что пароль содержит не менее 8 символов",
"repeatPasswordHint": "Введите пароль еще раз",
"repeatPassword": "Повторите пароль",
"passwordForgottenQuestion": "Забыли пароль?",
"passwordResetLabel": "Сбросить пароль",
"passwordResetSubheading": "Введите ваш email ниже. Вы получите ссылку для сброса пароля.",
"passwordResetSuccessMessage": "Проверьте почту! Мы отправили вам ссылку для сброса пароля.",
"passwordRecoveredQuestion": "Восстановили пароль?",
"passwordLengthError": "Пожалуйста, введите пароль длиной не менее 6 символов",
"sendEmailLink": "Отправить ссылку на почту",
"sendingEmailLink": "Отправка ссылки...",
"sendLinkSuccessDescription": "Проверьте вашу почту, мы только что отправили вам ссылку. Перейдите по ней, чтобы войти.",
"sendLinkSuccess": "Мы отправили вам ссылку по электронной почте",
"sendLinkSuccessToast": "Ссылка успешно отправлена",
"getNewLink": "Получить новую ссылку",
"verifyCodeHeading": "Подтвердите ваш аккаунт",
"verificationCode": "Код подтверждения",
"verificationCodeHint": "Введите код, который мы отправили вам по SMS",
"verificationCodeSubmitButtonLabel": "Отправить код",
"sendingMfaCode": "Отправка кода подтверждения...",
"verifyingMfaCode": "Проверка кода...",
"sendMfaCodeError": "Извините, не удалось отправить код подтверждения",
"verifyMfaCodeSuccess": "Код подтвержден! Входим в систему...",
"verifyMfaCodeError": "Упс! Похоже, код неверный",
"reauthenticate": "Повторная аутентификация",
"reauthenticateDescription": "В целях безопасности вам нужно пройти повторную аутентификацию",
"errorAlertHeading": "Извините, не удалось вас аутентифицировать",
"emailConfirmationAlertHeading": "Мы отправили вам письмо для подтверждения.",
"emailConfirmationAlertBody": "Добро пожаловать! Пожалуйста, проверьте вашу почту и перейдите по ссылке, чтобы подтвердить аккаунт.",
"resendLink": "Отправить ссылку снова",
"resendLinkSuccessDescription": "Мы отправили новую ссылку на вашу почту! Перейдите по ней, чтобы войти.",
"resendLinkSuccess": "Проверьте вашу почту!",
"authenticationErrorAlertHeading": "Ошибка аутентификации",
"authenticationErrorAlertBody": "Извините, не удалось вас аутентифицировать. Пожалуйста, попробуйте снова.",
"sendEmailCode": "Получить код на Email",
"sendingEmailCode": "Отправка кода...",
"resetPasswordError": "Извините, не удалось сбросить пароль. Пожалуйста, попробуйте снова.",
"emailPlaceholder": "your@email.com",
"inviteAlertHeading": "You have been invited to join a company",
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"orContinueWith": "Or continue with",
"redirecting": "You're in! Please wait...",
"inviteAlertHeading": "Вас пригласили присоединиться к компании",
"inviteAlertBody": "Пожалуйста, войдите или зарегистрируйтесь, чтобы принять приглашение и присоединиться к компании.",
"acceptTermsAndConditions": "Я принимаю <TermsOfServiceLink /> и <PrivacyPolicyLink />",
"termsOfService": "Условия использования",
"privacyPolicy": "Политика конфиденциальности",
"orContinueWith": "Или продолжить через",
"redirecting": "Вы вошли! Пожалуйста, подождите...",
"errors": {
"Invalid login credentials": "The credentials entered are invalid",
"User already registered": "This credential is already in use. Please try with another one.",
"Email not confirmed": "Please confirm your email address before signing in",
"default": "We have encountered an error. Please ensure you have a working internet connection and try again",
"generic": "Sorry, we weren't able to authenticate you. Please try again.",
"link": "Sorry, we encountered an error while sending your link. Please try again.",
"codeVerifierMismatch": "It looks like you're trying to sign in using a different browser than the one you used to request the sign in link. Please try again using the same browser.",
"minPasswordLength": "Password must be at least 8 characters long",
"passwordsDoNotMatch": "The passwords do not match",
"minPasswordNumbers": "Password must contain at least one number",
"minPasswordSpecialChars": "Password must contain at least one special character",
"uppercasePassword": "Password must contain at least one uppercase letter",
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action",
"otp_expired": "The email link has expired. Please try again.",
"same_password": "The password cannot be the same as the current password"
"Invalid login credentials": "Введенные данные недействительны",
"User already registered": "Эти данные уже используются. Пожалуйста, попробуйте другие.",
"Email not confirmed": "Пожалуйста, подтвердите ваш email перед входом",
"default": "Произошла ошибка. Убедитесь, что у вас есть подключение к интернету, и попробуйте снова",
"generic": "Извините, не удалось вас аутентифицировать. Пожалуйста, попробуйте снова.",
"link": "Извините, произошла ошибка при отправке ссылки. Пожалуйста, попробуйте снова.",
"codeVerifierMismatch": "Похоже, вы пытаетесь войти через другой браузер, чем тот, в котором запрашивали ссылку для входа. Пожалуйста, используйте тот же браузер.",
"minPasswordLength": "Пароль должен содержать не менее 8 символов",
"passwordsDoNotMatch": "Пароли не совпадают",
"minPasswordNumbers": "Пароль должен содержать хотя бы одну цифру",
"minPasswordSpecialChars": "Пароль должен содержать хотя бы один специальный символ",
"uppercasePassword": "Пароль должен содержать хотя бы одну заглавную букву",
"insufficient_aal": "Пожалуйста, войдите с текущей многофакторной аутентификацией, чтобы выполнить это действие",
"otp_expired": "Срок действия ссылки на email истек. Пожалуйста, попробуйте снова.",
"same_password": "Пароль не может совпадать с текущим паролем"
}
}

View File

@@ -1,123 +1,143 @@
{
"subscriptionTabSubheading": "Manage your Subscription and Billing",
"planCardTitle": "Your Plan",
"planCardDescription": "Below are the details of your current plan. You can change your plan or cancel your subscription at any time.",
"planRenewal": "Renews every {{interval}} at {{price}}",
"planDetails": "Plan Details",
"checkout": "Proceed to Checkout",
"trialEndsOn": "Your trial ends on",
"billingPortalCardButton": "Visit Billing Portal",
"billingPortalCardTitle": "Manage your Billing Details",
"billingPortalCardDescription": "Visit your Billing Portal to manage your subscription and billing. You can update or cancel your plan, or download your invoices.",
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
"noPermissionsAlertBody": "Please contact your account admin to change the billing settings for your account.",
"checkoutSuccessTitle": "Done! You're all set.",
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
"checkoutSuccessBackButton": "Proceed to App",
"cannotManageBillingAlertTitle": "You cannot manage billing",
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account admin.",
"manageTeamPlan": "Manage your Company Plan",
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
"basePlan": "Base Plan",
"subscriptionTabSubheading": "Управление подпиской и оплатой",
"planCardTitle": "Ваш тариф",
"planCardDescription": "Ниже приведены детали вашего текущего тарифа. Вы можете изменить тариф или отменить подписку в любое время.",
"planRenewal": "Продлевается каждые {{interval}} за {{price}}",
"planDetails": "Детали тарифа",
"checkout": "Перейти к оплате",
"trialEndsOn": "Пробный период заканчивается",
"billingPortalCardButton": "Перейти в биллинг-портал",
"billingPortalCardTitle": "Управление платежными данными",
"billingPortalCardDescription": "Перейдите в биллинг-портал, чтобы управлять подпиской и платежами. Вы можете изменить или отменить тариф, а также скачать счета.",
"cancelAtPeriodEndDescription": "Ваша подписка будет отменена {{- endDate }}.",
"renewAtPeriodEndDescription": "Ваша подписка будет продлена {{- endDate }}",
"noPermissionsAlertHeading": "У вас нет прав для изменения настроек оплаты",
"noPermissionsAlertBody": "Пожалуйста, свяжитесь с администратором аккаунта, чтобы изменить настройки оплаты.",
"checkoutSuccessTitle": "Готово! Всё настроено.",
"checkoutSuccessDescription": "Спасибо за подписку! Мы успешно оформили вашу подписку. Подтверждение отправлено на {{ customerEmail }}.",
"checkoutSuccessBackButton": "Перейти в приложение",
"cannotManageBillingAlertTitle": "Вы не можете управлять оплатой",
"cannotManageBillingAlertDescription": "У вас нет прав для управления оплатой. Пожалуйста, свяжитесь с администратором аккаунта.",
"manageTeamPlan": "Управление корпоративным тарифом",
"manageTeamPlanDescription": "Выберите тариф, который соответствует потребностям вашей компании. Вы можете изменить его в любое время.",
"basePlan": "Базовый тариф",
"billingInterval": {
"label": "Choose your billing interval",
"month": "Billed monthly",
"year": "Billed yearly"
"label": "Выберите интервал оплаты",
"month": "Оплата ежемесячно",
"year": "Оплата ежегодно"
},
"perMonth": "month",
"custom": "Custom Plan",
"lifetime": "Lifetime",
"trialPeriod": "{{period}} day trial",
"perPeriod": "per {{period}}",
"redirectingToPayment": "Redirecting to checkout. Please wait...",
"proceedToPayment": "Proceed to Payment",
"startTrial": "Start Trial",
"perTeamMember": "Per company member",
"perUnit": "Per {{unit}} usage",
"teamMembers": "Company Members",
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
"andAbove": "above {{ previousTier }} {{ unit }}",
"startingAtPriceUnit": "Starting at {{price}}/{{unit}}",
"perMonth": "месяц",
"custom": "Индивидуальный тариф",
"lifetime": "Пожизненный",
"trialPeriod": "Пробный период {{period}} дней",
"perPeriod": "за {{period}}",
"redirectingToPayment": "Перенаправление на оплату. Пожалуйста, подождите...",
"proceedToPayment": "Перейти к оплате",
"startTrial": "Начать пробный период",
"perTeamMember": "На одного сотрудника",
"perUnit": "За использование {{unit}}",
"teamMembers": "Сотрудники компании",
"includedUpTo": "До {{upTo}} {{unit}} включено в тариф",
"fromPreviousTierUpTo": "за каждый {{unit}} для следующих {{ upTo }} {{ unit }}",
"andAbove": "свыше {{ previousTier }} {{ unit }}",
"startingAtPriceUnit": "Начиная с {{price}}/{{unit}}",
"priceUnit": "{{price}}/{{unit}}",
"forEveryUnit": "for every {{ unit }}",
"setupFee": "plus a {{ setupFee }} setup fee",
"perUnitIncluded": "({{included}} included)",
"featuresLabel": "Features",
"detailsLabel": "Details",
"planPickerLabel": "Pick your preferred plan",
"planCardLabel": "Manage your Plan",
"planPickerAlertErrorTitle": "Error requesting checkout",
"planPickerAlertErrorDescription": "There was an error requesting checkout. Please try again later.",
"subscriptionCancelled": "Subscription Cancelled",
"cancelSubscriptionDate": "Your subscription will be cancelled at the end of the period",
"noPlanChosen": "Please choose a plan",
"noIntervalPlanChosen": "Please choose a billing interval",
"forEveryUnit": "за каждый {{ unit }}",
"setupFee": "плюс плата за установку {{ setupFee }}",
"perUnitIncluded": "({{included}} включено)",
"featuresLabel": "Функции",
"detailsLabel": "Подробности",
"planPickerLabel": "Выберите тариф",
"planCardLabel": "Управление тарифом",
"planPickerAlertErrorTitle": "Ошибка при запросе оплаты",
"planPickerAlertErrorDescription": "Произошла ошибка при запросе оплаты. Пожалуйста, попробуйте позже.",
"subscriptionCancelled": "Подписка отменена",
"cancelSubscriptionDate": "Ваша подписка будет отменена в конце расчетного периода",
"noPlanChosen": "Пожалуйста, выберите тариф",
"noIntervalPlanChosen": "Пожалуйста, выберите интервал оплаты",
"status": {
"free": {
"badge": "Free Plan",
"heading": "You are currently on the Free Plan",
"description": "You're on a free plan. You can upgrade to a paid plan at any time."
"badge": "Бесплатный тариф",
"heading": "Вы используете бесплатный тариф",
"description": "Вы на бесплатном тарифе. Вы можете перейти на платный тариф в любое время."
},
"active": {
"badge": "Active",
"heading": "Your subscription is active",
"description": "Your subscription is active. You can manage your subscription and billing in the Customer Portal."
"badge": "Активен",
"heading": "Ваша подписка активна",
"description": "Ваша подписка активна. Вы можете управлять подпиской и оплатой в клиентском портале."
},
"trialing": {
"badge": "Trial",
"heading": "You're on a trial",
"description": "You can enjoy the benefits of plan until the trial ends"
"badge": "Пробный период",
"heading": "Вы используете пробный период",
"description": "Вы можете пользоваться тарифом до окончания пробного периода"
},
"past_due": {
"badge": "Past Due",
"heading": "Your invoice is past due",
"description": "Your invoice is past due. Please update your payment method."
"badge": "Просрочено",
"heading": "Ваш счет просрочен",
"description": "Ваш счет просрочен. Пожалуйста, обновите способ оплаты."
},
"canceled": {
"badge": "Canceled",
"heading": "Your subscription is canceled",
"description": "Your subscription is canceled. It is scheduled to end at end of the billing period."
"badge": "Отменено",
"heading": "Ваша подписка отменена",
"description": "Ваша подписка отменена и завершится в конце расчетного периода."
},
"unpaid": {
"badge": "Unpaid",
"heading": "Your invoice is unpaid",
"description": "Your invoice is unpaid. Please update your payment method."
"badge": "Неоплачено",
"heading": "Ваш счет не оплачен",
"description": "Ваш счет не оплачен. Пожалуйста, обновите способ оплаты."
},
"incomplete": {
"badge": "Incomplete",
"heading": "We're waiting for your payment",
"description": "We're waiting for your payment to go through. Please bear with us."
"badge": "Не завершено",
"heading": "Мы ждем вашу оплату",
"description": "Мы ждем завершения вашей оплаты. Пожалуйста, подождите."
},
"incomplete_expired": {
"badge": "Expired",
"heading": "Your payment has expired",
"description": "Your payment has expired. Please update your payment method."
"badge": "Истек",
"heading": "Срок оплаты истек",
"description": "Срок оплаты истек. Пожалуйста, обновите способ оплаты."
},
"paused": {
"badge": "Paused",
"heading": "Your subscription is paused",
"description": "Your subscription is paused. You can resume it at any time."
"badge": "Приостановлено",
"heading": "Ваша подписка приостановлена",
"description": "Ваша подписка приостановлена. Вы можете возобновить ее в любое время."
},
"succeeded": {
"badge": "Succeeded",
"heading": "Your payment was successful",
"description": "Your payment was successful. Thank you for subscribing!"
"badge": "Успешно",
"heading": "Оплата прошла успешно",
"description": "Оплата прошла успешно. Спасибо за подписку!"
},
"pending": {
"badge": "Pending",
"heading": "Your payment is pending",
"description": "Your payment is pending. Please bear with us."
"badge": "В ожидании",
"heading": "Оплата в ожидании",
"description": "Оплата находится в ожидании. Пожалуйста, подождите."
},
"failed": {
"badge": "Failed",
"heading": "Your payment failed",
"description": "Your payment failed. Please update your payment method."
"badge": "Неудачно",
"heading": "Оплата не прошла",
"description": "Оплата не прошла. Пожалуйста, обновите способ оплаты."
}
},
"cart": {
"label": "Cart ({{ items }})"
"label": "Корзина ({{ items }})"
},
"pageTitle": "Бюджет {{companyName}}",
"description": "Выберите подходящую дату в календаре и запишитесь на прием.",
"saveChanges": "Сохранить изменения",
"healthBenefitForm": {
"title": "Форма здоровья",
"description": "Поддержка сотрудника из корпоративного фонда здоровья",
"info": "* К ценам добавляются государственные налоги"
},
"occurance": {
"yearly": "Раз в год",
"quarterly": "Раз в квартал",
"monthly": "Раз в месяц"
},
"expensesOverview": {
"title": "Обзор расходов за 2025 год",
"monthly": "Расход на одного сотрудника в месяц *",
"yearly": "Максимальный расход на одного человека в год *",
"total": "Максимальный расход на {{employeeCount}} сотрудников в год *",
"sum": "Итого"
}
}

View File

@@ -1,8 +1,9 @@
{
"title": "Select service",
"description": "Select the appropriate service or package according to your health needs or goals.",
"analysisPackages": {
"title": "Analysis packages",
"description": "Get to know the personal analysis packages and order"
}
"title": "Выберите услугу",
"description": "Выберите подходящую услугу или пакет в зависимости от вашей проблемы со здоровьем или цели.",
"analysisPackages": {
"title": "Пакеты анализов",
"description": "Ознакомьтесь с персональными пакетами анализов и закажите"
},
"noCategories": "Список услуг не найден, попробуйте позже"
}

View File

@@ -0,0 +1,75 @@
{
"title": "Корзина",
"description": "Просмотрите свою корзину",
"emptyCartMessage": "Ваша корзина пуста",
"emptyCartMessageDescription": "Добавьте товары в корзину, чтобы продолжить.",
"subtotal": "Промежуточный итог",
"total": "Сумма",
"table": {
"item": "Товар",
"quantity": "Количество",
"price": "Цена",
"total": "Сумма"
},
"checkout": {
"goToCheckout": "Оформить заказ",
"goToDashboard": "Продолжить",
"error": {
"title": "Что-то пошло не так",
"description": "Пожалуйста, попробуйте позже."
},
"timeLeft": "Осталось времени {{timeLeft}}",
"timeoutTitle": "Бронирование истекло",
"timeoutDescription": "Бронирование товара {{productTitle}} в корзине истекло.",
"timeoutAction": "Продолжить"
},
"discountCode": {
"title": "Подарочная карта или промокод",
"label": "Добавить промокод",
"apply": "Применить",
"subtitle": "Если хотите, можете добавить промокод",
"placeholder": "Введите промокод"
},
"items": {
"synlabAnalyses": {
"productColumnLabel": "Название анализа"
},
"ttoServices": {
"productColumnLabel": "Название услуги"
},
"delete": {
"success": "Товар удален из корзины",
"loading": "Удаление товара из корзины",
"error": "Не удалось удалить товар из корзины"
},
"analysisLocation": {
"success": "Местоположение обновлено",
"loading": "Обновление местоположения",
"error": "Не удалось обновить местоположение"
}
},
"order": {
"title": "Заказ"
},
"orderConfirmed": {
"title": "Заказ успешно оформлен",
"summary": "Услуги",
"subtotal": "Промежуточный итог",
"taxes": "Налоги",
"giftCard": "Подарочная карта",
"total": "Сумма",
"orderDate": "Дата заказа",
"orderNumber": "Номер заказа",
"orderStatus": "Статус заказа",
"paymentStatus": "Статус оплаты"
},
"montonioCallback": {
"title": "Процесс оплаты Montonio",
"description": "Пожалуйста, подождите, пока мы завершим обработку вашего платежа."
},
"locations": {
"title": "Местоположение для сдачи анализов",
"description": "Если у вас нет возможности прийти в выбранное место для сдачи анализов, вы можете посетить любой удобный для вас пункт забора крови.",
"locationSelect": "Выберите местоположение"
}
}

View File

@@ -1,118 +1,140 @@
{
"homeTabLabel": "Home",
"homeTabDescription": "Welcome to your home page",
"accountMembers": "Company Members",
"membersTabDescription": "Here you can manage the members of your company.",
"billingTabLabel": "Billing",
"billingTabDescription": "Manage your billing and subscription",
"dashboardTabLabel": "Dashboard",
"settingsTabLabel": "Settings",
"profileSettingsTabLabel": "Profile",
"subscriptionSettingsTabLabel": "Subscription",
"dashboardTabDescription": "An overview of your account's activity and performance across all your projects.",
"settingsTabDescription": "Manage your settings and preferences.",
"emailAddress": "Email Address",
"password": "Password",
"modalConfirmationQuestion": "Are you sure you want to continue?",
"imageInputLabel": "Click here to upload an image",
"cancel": "Cancel",
"clear": "Clear",
"close": "Close",
"notFound": "Not Found",
"backToHomePage": "Back to Home Page",
"goBack": "Go Back",
"genericServerError": "Sorry, something went wrong.",
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
"pageNotFound": "Sorry, this page does not exist.",
"pageNotFoundSubHeading": "Apologies, the page you were looking for was not found",
"genericError": "Sorry, something went wrong.",
"genericErrorSubHeading": "Apologies, an error occurred while processing your request. Please contact us if the issue persists.",
"anonymousUser": "Anonymous",
"tryAgain": "Try Again",
"theme": "Theme",
"lightTheme": "Light",
"darkTheme": "Dark",
"systemTheme": "System",
"expandSidebar": "Expand Sidebar",
"collapseSidebar": "Collapse Sidebar",
"documentation": "Documentation",
"getStarted": "Get Started",
"getStartedWithPlan": "Get Started with {{plan}}",
"retry": "Retry",
"contactUs": "Contact Us",
"loading": "Loading. Please wait...",
"yourAccounts": "Your Accounts",
"continue": "Continue",
"skip": "Skip",
"signedInAs": "Signed in as",
"pageOfPages": "Page {{page}} of {{total}}",
"noData": "No data available",
"pageNotFoundHeading": "Ouch! :|",
"errorPageHeading": "Ouch! :|",
"notifications": "Notifications",
"noNotifications": "No notifications",
"justNow": "Just now",
"newVersionAvailable": "New version available",
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
"newVersionSubmitButton": "Reload and Update",
"back": "Back",
"welcome": "Welcome",
"shoppingCart": "Shopping cart",
"search": "Search{{end}}",
"myActions": "My actions",
"homeTabLabel": "Главная",
"homeTabDescription": "Добро пожаловать на вашу домашнюю страницу",
"accountMembers": "Члены компании",
"membersTabDescription": "Здесь вы можете управлять членами вашей компании.",
"billingTabLabel": "Оплата",
"billingTabDescription": "Управление оплатой и подпиской",
"dashboardTabLabel": "Панель",
"settingsTabLabel": "Настройки",
"profileSettingsTabLabel": "Профиль",
"subscriptionSettingsTabLabel": "Подписка",
"dashboardTabDescription": "Обзор активности и эффективности вашей учетной записи по всем проектам.",
"settingsTabDescription": "Управляйте своими настройками и предпочтениями.",
"emailAddress": "Электронная почта",
"password": "Пароль",
"modalConfirmationQuestion": "Вы уверены, что хотите продолжить?",
"imageInputLabel": "Нажмите здесь, чтобы загрузить изображение",
"cancel": "Отмена",
"clear": "Очистить",
"close": "Закрыть",
"notFound": "Не найдено",
"backToHomePage": "Вернуться на главную",
"goBack": "Назад",
"genericServerError": "Извините, что-то пошло не так.",
"genericServerErrorHeading": "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема не исчезнет.",
"pageNotFound": "Извините, эта страница не существует.",
"pageNotFoundSubHeading": "К сожалению, запрашиваемая страница не найдена",
"genericError": "Извините, что-то пошло не так.",
"genericErrorSubHeading": "К сожалению, произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема не исчезнет.",
"anonymousUser": "Анонимный пользователь",
"tryAgain": "Попробовать снова",
"theme": "Тема",
"lightTheme": "Светлая",
"darkTheme": "Тёмная",
"systemTheme": "Системная",
"expandSidebar": "Развернуть боковое меню",
"collapseSidebar": "Свернуть боковое меню",
"documentation": "Документация",
"getStarted": "Начать!",
"getStartedWithPlan": "Начать с {{plan}}",
"retry": "Повторить",
"contactUs": "Свяжитесь с нами",
"loading": "Загрузка. Пожалуйста, подождите...",
"yourAccounts": "Ваши аккаунты",
"continue": "Продолжить",
"skip": "Пропустить",
"signedInAs": "Вы вошли как",
"pageOfPages": "Страница {{page}} / {{total}}",
"noData": "Нет данных",
"pageNotFoundHeading": "Упс! :|",
"errorPageHeading": "Упс! :|",
"notifications": "Уведомления",
"noNotifications": "Нет уведомлений",
"justNow": "Прямо сейчас",
"newVersionAvailable": "Доступна новая версия",
"newVersionAvailableDescription": "Доступна новая версия приложения. Рекомендуется обновить страницу, чтобы получить последние обновления и избежать возможных проблем.",
"newVersionSubmitButton": "Перезагрузить и обновить",
"back": "Назад",
"welcome": "Добро пожаловать",
"shoppingCart": "Корзина",
"shoppingCartCount": "Корзина ({{count}})",
"search": "Поиск{{end}}",
"myActions": "Мои действия",
"healthPackageComparison": {
"label": "Health package comparison",
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
"label": "Сравнение пакетов здоровья",
"description": "Ниже приведен персональный выбор пакета медицинского обследования на основе предварительной информации (пол, возраст и индекс массы тела). В таблице можно добавить к рекомендуемому пакету отдельные исследования."
},
"routes": {
"home": "Home",
"overview": "Overview",
"booking": "Booking",
"myOrders": "My orders",
"orderAnalysis": "Order analysis",
"orderAnalysisPackage": "Order analysis package",
"orderHealthAnalysis": "Order health analysis",
"account": "Account",
"members": "Members",
"billing": "Billing",
"dashboard": "Dashboard",
"settings": "Settings",
"profile": "Profile",
"application": "Application"
"home": "Главная",
"overview": "Обзор",
"booking": "Забронировать время",
"myOrders": "Мои заказы",
"analysisResults": "Результаты анализов",
"orderAnalysisPackage": "Заказать пакет анализов",
"orderAnalysis": "Заказать анализ",
"orderHealthAnalysis": "Заказать обследование",
"account": "Аккаунт",
"members": "Участники",
"billing": "Оплата",
"dashboard": "Обзор",
"settings": "Настройки",
"profile": "Профиль",
"application": "Приложение",
"pickTime": "Выберите время"
},
"roles": {
"owner": {
"label": "Admin"
"label": "Администратор"
},
"member": {
"label": "Member"
"label": "Участник"
}
},
"otp": {
"requestVerificationCode": "Request Verification Code",
"requestVerificationCodeDescription": "We must verify your identity to continue with this action. We'll send a verification code to the email address {{email}}.",
"sendingCode": "Sending Code...",
"sendVerificationCode": "Send Verification Code",
"enterVerificationCode": "Enter Verification Code",
"codeSentToEmail": "We've sent a verification code to the email address {{email}}.",
"verificationCode": "Verification Code",
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email.",
"verifying": "Verifying...",
"verifyCode": "Verify Code",
"requestNewCode": "Request New Code",
"errorSendingCode": "Error sending code. Please try again."
"requestVerificationCode": "Запросить код подтверждения",
"requestVerificationCodeDescription": "Мы должны подтвердить вашу личность для продолжения. Мы отправим код подтверждения на электронный адрес {{email}}.",
"sendingCode": "Отправка кода...",
"sendVerificationCode": "Отправить код подтверждения",
"enterVerificationCode": "Введите код подтверждения",
"codeSentToEmail": "Мы отправили код подтверждения на электронный адрес {{email}}.",
"verificationCode": "Код подтверждения",
"enterCodeFromEmail": "Введите 6-значный код, который мы отправили на вашу почту.",
"verifying": "Проверка...",
"verifyCode": "Подтвердить код",
"requestNewCode": "Запросить новый код",
"errorSendingCode": "Ошибка при отправке кода. Пожалуйста, попробуйте снова."
},
"cookieBanner": {
"title": "Hey, we use cookies 🍪",
"description": "This website uses cookies to ensure you get the best experience on our website.",
"reject": "Reject",
"accept": "Accept"
"title": "Привет, мы используем куки 🍪",
"description": "Этот сайт использует файлы cookie, чтобы обеспечить вам наилучший опыт.",
"reject": "Отклонить",
"accept": "Принять"
},
"doctor": "Doctor",
"save": "Save",
"saveAsDraft": "Save as draft",
"confirm": "Confirm",
"previous": "Previous",
"next": "Next",
"invalidDataError": "Invalid data submitted"
"formField": {
"companyName": "Название компании",
"contactPerson": "Контактное лицо",
"email": "Электронная почта",
"phone": "Телефон",
"firstName": "Имя",
"lastName": "Фамилия",
"personalCode": "Личный код",
"city": "Город",
"weight": "Вес",
"height": "Рост",
"occurance": "Частота поддержки",
"amount": "Сумма",
"selectDate": "Выберите дату"
},
"wallet": {
"balance": "Баланс вашего MedReport аккаунта",
"expiredAt": "Действительно до {{expiredAt}}"
},
"doctor": "Врач",
"save": "Сохранить",
"saveAsDraft": "Сохранить как черновик",
"confirm": "Подтвердить",
"previous": "Предыдущий",
"next": "Следующий",
"invalidDataError": "Некорректные данные"
}

View File

@@ -1,16 +1,22 @@
{
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
"respondToQuestion": "Respond",
"gender": "Gender",
"male": "Male",
"female": "Female",
"age": "Age",
"height": "Height",
"weight": "Weight",
"bmi": "BMI",
"bloodPressure": "Blood pressure",
"cholesterol": "Cholesterol",
"ldlCholesterol": "LDL Cholesterol",
"smoking": "Smoking",
"recommendedForYou": "Recommended for you"
"recentlyCheckedDescription": "Отлично, вы проверили своё здоровье. Вот важные показатели для вас",
"respondToQuestion": "Ответить на вопрос",
"gender": "Пол",
"male": "Мужчина",
"female": "Женщина",
"age": "Возраст",
"height": "Рост",
"weight": "Вес",
"bmi": "ИМТ",
"bloodPressure": "Давление",
"cholesterol": "Холестерин",
"ldlCholesterol": "ЛПНП холестерин",
"smoking": "Курение",
"recommendedForYou": "Рекомендации для вас",
"heroCard": {
"orderAnalysis": {
"title": "Заказать анализ",
"description": "Закажите подходящий для вас анализ"
}
}
}

View File

@@ -1,50 +1,51 @@
{
"sidebar": {
"dashboard": "Overview",
"openReviews": "Open jobs",
"myReviews": "My jobs",
"completedReviews": "Completed jobs"
"dashboard": "Обзор",
"openReviews": "Свободные задания",
"myReviews": "Мои задания",
"completedReviews": "Выполненные задания"
},
"openReviews": "Open jobs",
"myReviews": "My jobs",
"completedReviews": "Completed jobs",
"otherReviews": "Other jobs",
"openReviews": "Свободные задания",
"myReviews": "Мои задания",
"completedReviews": "Выполненные задания",
"otherReviews": "Другие задания",
"resultsTable": {
"patientName": "Patient name",
"serviceName": "Service",
"orderNr": "Order number",
"time": "Time",
"assignedTo": "Doctor",
"resultsStatus": "Analysis results",
"waitingForNr": "Waiting for {{nr}}",
"responsesReceived": "Results complete"
"patientName": "Имя пациента",
"serviceName": "Услуга",
"orderNr": "Номер заказа",
"time": "Время",
"assignedTo": "Врач",
"resultsStatus": "Результаты анализов",
"waitingForNr": "В ожидании {{nr}}",
"language": "Предпочтительный язык",
"responsesReceived": "Результаты готовы"
},
"otherPatients": "Other patients",
"analyses": "Analyses",
"open": "Open",
"name": "Name",
"personalCode": "Personal code",
"dobAndAge": "Date of birth and age",
"bmi": "Body mass index",
"smoking": "Smoking",
"phone": "Phone",
"email": "Email",
"results": "Analysis results",
"feedback": "Summary",
"selectJob": "Select",
"unselectJob": "Unselect",
"previousResults": "Previous results ({{date}})",
"labComment": "Lab comment",
"otherPatients": "Другие пациенты",
"analyses": "Анализы",
"open": "Открыть",
"name": "Имя",
"personalCode": "Личный код",
"dobAndAge": "Дата рождения и возраст",
"bmi": "Индекс массы тела",
"smoking": "Курение",
"phone": "Телефон",
"email": "Эл. почта",
"results": "Результаты анализов",
"feedback": "Заключение",
"selectJob": "Выбрать",
"unselectJob": "Отказаться",
"previousResults": "Предыдущие результаты ({{date}})",
"labComment": "Комментарии лаборатории",
"confirmFeedbackModal": {
"title": "Confirm publishing summary",
"description": "When confirmed, the summary will be published to the patient."
"title": "Подтвердите публикацию заключения",
"description": "После подтверждения заключение будет опубликовано для пациента."
},
"error": {
"UNKNOWN": "Something went wrong",
"JOB_ASSIGNED": "Job already selected"
"UNKNOWN": "Что-то пошло не так",
"JOB_ASSIGNED": "Задание уже занято"
},
"updateFeedbackSuccess": "Summary updated",
"updateFeedbackLoading": "Updating summary...",
"updateFeedbackError": "Failed to update summary",
"feedbackLengthError": "Summary must be at least 10 characters"
"updateFeedbackSuccess": "Заключение обновлено",
"updateFeedbackLoading": "Обновление заключения...",
"updateFeedbackError": "Не удалось обновить заключение",
"feedbackLengthError": "Заключение должно содержать не менее 10 символов"
}

View File

@@ -1,41 +1,41 @@
{
"blog": "Blog",
"blogSubtitle": "News and updates about the platform",
"documentation": "Documentation",
"documentationSubtitle": "Tutorials and guide to get started with the platform",
"blog": "Блог",
"blogSubtitle": "Новости и обновления о платформе",
"documentation": "Документация",
"documentationSubtitle": "Учебники и руководство по началу работы с платформой",
"faq": "FAQ",
"faqSubtitle": "Frequently asked questions about the platform",
"pricing": "Pricing",
"pricingSubtitle": "Pricing plans and payment options",
"backToBlog": "Back to blog",
"noPosts": "No posts found",
"blogPaginationNext": "Next Page",
"blogPaginationPrevious": "Previous Page",
"readMore": "Read more",
"contactFaq": "If you have any questions, please contact us",
"contact": "Contact",
"about": "About",
"product": "Product",
"legal": "Legal",
"termsOfService": "Terms of Service",
"termsOfServiceDescription": "Our terms and conditions",
"cookiePolicy": "Cookie Policy",
"cookiePolicyDescription": "Our cookie policy and how we use them",
"privacyPolicy": "Privacy Policy",
"privacyPolicyDescription": "Our privacy policy and how we use your data",
"contactDescription": "Contact us for any questions or feedback",
"contactHeading": "Send us a message",
"contactSubheading": "We will get back to you as soon as possible",
"contactName": "Your Name",
"contactEmail": "Your Email",
"contactMessage": "Your Message",
"sendMessage": "Send Message",
"contactSuccess": "Your message has been sent successfully",
"contactError": "An error occurred while sending your message",
"contactSuccessDescription": "We have received your message and will get back to you as soon as possible",
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
"footerDescription": "Here you can add a description about your company or product",
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
"heroSubtitle": "A simple, convenient, and quick overview of your health condition",
"notInterestedInAudit": "Currently not interested in a health audit"
"faqSubtitle": "Часто задаваемые вопросы о платформе",
"pricing": "Цены",
"pricingSubtitle": "Тарифные планы и варианты оплаты",
"backToBlog": "Назад к блогу",
"noPosts": "Посты не найдены",
"blogPaginationNext": "Следующая страница",
"blogPaginationPrevious": "Предыдущая страница",
"readMore": "Читать далее",
"contactFaq": "Если у вас есть вопросы, пожалуйста, свяжитесь с нами",
"contact": "Контакты",
"about": "О нас",
"product": "Продукт",
"legal": "Правовая информация",
"termsOfService": "Условия обслуживания",
"termsOfServiceDescription": "Наши правила и условия",
"cookiePolicy": "Политика использования файлов cookie",
"cookiePolicyDescription": "Наша политика использования cookie и как мы их применяем",
"privacyPolicy": "Политика конфиденциальности",
"privacyPolicyDescription": "Наша политика конфиденциальности и как мы используем ваши данные",
"contactDescription": "Свяжитесь с нами по любым вопросам или отзывам",
"contactHeading": "Отправьте нам сообщение",
"contactSubheading": "Мы свяжемся с вами как можно скорее",
"contactName": "Ваше имя",
"contactEmail": "Ваш email",
"contactMessage": "Ваше сообщение",
"sendMessage": "Отправить сообщение",
"contactSuccess": "Ваше сообщение успешно отправлено",
"contactError": "Произошла ошибка при отправке сообщения",
"contactSuccessDescription": "Мы получили ваше сообщение и свяжемся с вами в ближайшее время",
"contactErrorDescription": "Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте позже",
"footerDescription": "Здесь вы можете добавить описание вашей компании или продукта",
"copyright": "© Copyright {{year}} {{product}}. Все права защищены.",
"heroSubtitle": "Простой, удобный и быстрый обзор вашего здоровья",
"notInterestedInAudit": "Не хочу проходить аудит здоровья в данный момент"
}

View File

@@ -1,7 +1,9 @@
{
"title": "Select analysis package",
"noPackagesAvailable": "No packages available",
"selectThisPackage": "Select this package",
"selectPackage": "Select package",
"comparePackages": "Compare packages"
"title": "Выберите пакет анализов",
"noPackagesAvailable": "Список услуг не найден, попробуйте позже",
"selectThisPackage": "Выбрать этот пакет",
"selectPackage": "Выберите пакет",
"comparePackages": "Сравнить пакеты",
"analysisPackageAddedToCart": "Пакет анализов добавлен в корзину",
"analysisPackageAddToCartError": "Не удалось добавить пакет анализов в корзину"
}

View File

@@ -0,0 +1,7 @@
{
"title": "Выберите анализ",
"description": "Результаты всех анализов будут доступны в течение 13 рабочих дней после сдачи крови.",
"analysisNotAvailable": "Заказ анализа в данный момент недоступен",
"analysisAddedToCart": "Анализ добавлен в корзину",
"analysisAddToCartError": "Не удалось добавить анализ в корзину"
}

View File

@@ -0,0 +1,19 @@
{
"title": "Заказы",
"description": "Просмотрите ваши заказы",
"table": {
"analysisPackage": "Пакет анализов",
"otherOrders": "Заказ",
"createdAt": "Дата заказа",
"status": "Статус"
},
"status": {
"QUEUED": "Отправлено",
"PROCESSING": "Передано в Synlab",
"PARTIAL_ANALYSIS_RESPONSE": "Частичные результаты",
"FULL_ANALYSIS_RESPONSE": "Все результаты получены, ожидается заключение врача",
"COMPLETED": "Подтверждено",
"REJECTED": "Возвращено",
"CANCELLED": "Отменено"
}
}

View File

@@ -1,31 +1,31 @@
{
"nrOfAnalyses": "{{nr}} analyses",
"nrOfAnalyses": "{{nr}} анализов",
"clinicalBloodDraw": {
"label": "Kliiniline vereanalüüs",
"description": "Pending"
"label": "Клинический анализ крови",
"description": "Ожидается"
},
"crp": {
"label": "C-reaktiivne valk (CRP)",
"description": "Pending"
"label": "С-реактивный белок (CRP)",
"description": "Ожидается"
},
"ferritin": {
"label": "Ferritiin",
"description": "Pending"
"label": "Ферритин",
"description": "Ожидается"
},
"vitaminD": {
"label": "D-vitamiin",
"description": "Pending"
"label": "Витамин D",
"description": "Ожидается"
},
"glucose": {
"label": "Glükoos",
"description": "Pending"
"label": "Глюкоза",
"description": "Ожидается"
},
"alat": {
"label": "Alaniini aminotransferaas",
"description": "Pending"
"label": "Аланинаминотрансфераза",
"description": "Ожидается"
},
"ast": {
"label": "Aspartaadi aminotransferaas",
"description": "Pending"
"label": "Аспартатаминотрансфераза",
"description": "Ожидается"
}
}

View File

@@ -1,164 +1,197 @@
{
"home": {
"pageTitle": "Home"
"pageTitle": "Обзор",
"headerTitle": "Обзор Tervisekassa {{companyName}}",
"healthDetails": "Данные о здоровье компании",
"membersSettingsButtonTitle": "Управление сотрудниками",
"membersSettingsButtonDescription": "Добавляйте, редактируйте или удаляйте сотрудников.",
"membersBillingButtonTitle": "Управление бюджетом",
"membersBillingButtonDescription": "Выберите, как распределять бюджет между сотрудниками."
},
"settings": {
"pageTitle": "Settings",
"pageDescription": "Manage your Company details",
"teamLogo": "Company Logo",
"teamLogoDescription": "Update your company's logo to make it easier to identify",
"teamName": "Company Name",
"teamNameDescription": "Update your company's name",
"dangerZone": "Danger Zone",
"dangerZoneDescription": "This section contains actions that are irreversible"
"pageTitle": "Настройки",
"pageDescription": "Управление данными вашей компании",
"teamLogo": "Логотип компании",
"teamLogoDescription": "Обновите логотип вашей компании для упрощения идентификации",
"teamName": "Название компании",
"teamNameDescription": "Обновите название вашей компании",
"dangerZone": "Опасная зона",
"dangerZoneDescription": "Этот раздел содержит действия, которые невозможно отменить"
},
"members": {
"pageTitle": "Members"
"pageTitle": "Сотрудники"
},
"billing": {
"pageTitle": "Billing"
"pageTitle": "Биллинг"
},
"yourTeams": "Your Companies ({{teamsCount}})",
"createTeam": "Create a Company",
"creatingTeam": "Creating Company...",
"personalAccount": "Personal Account",
"searchAccount": "Search Account...",
"membersTabLabel": "Members",
"memberName": "Name",
"youLabel": "You",
"emailLabel": "Email",
"roleLabel": "Role",
"primaryOwnerLabel": "Primary Admin",
"joinedAtLabel": "Joined at",
"invitedAtLabel": "Invited at",
"inviteMembersPageSubheading": "Invite members to your Company",
"createTeamModalHeading": "Create Company",
"createTeamModalDescription": "Create a new Company to manage your projects and members.",
"teamNameLabel": "Company Name",
"teamNameDescription": "Your company name should be unique and descriptive",
"createTeamSubmitLabel": "Create Company",
"createTeamSuccess": "Company created successfully",
"createTeamError": "Company not created. Please try again.",
"createTeamLoading": "Creating company...",
"settingsPageLabel": "General",
"createTeamDropdownLabel": "New company",
"changeRole": "Change Role",
"removeMember": "Remove from Account",
"inviteMembersSuccess": "Members invited successfully!",
"inviteMembersError": "Sorry, we encountered an error! Please try again",
"inviteMembersLoading": "Inviting members...",
"removeInviteButtonLabel": "Remove invite",
"addAnotherMemberButtonLabel": "Add another one",
"inviteMembersButtonLabel": "Send Invites",
"removeMemberModalHeading": "You are removing this user",
"removeMemberModalDescription": "Remove this member from the company. They will no longer have access to the company.",
"removeMemberSuccessMessage": "Member removed successfully",
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
"removeMemberLoadingMessage": "Removing member...",
"removeMemberSubmitLabel": "Remove User from Company",
"chooseDifferentRoleError": "Role is the same as the current one",
"updateRole": "Update Role",
"updateRoleLoadingMessage": "Updating role...",
"updateRoleSuccessMessage": "Role updated successfully",
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
"updateMemberRoleModalHeading": "Update Member's Role",
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
"roleMustBeDifferent": "Role must be different from the current one",
"memberRoleInputLabel": "Member role",
"updateRoleDescription": "Pick a role for this member.",
"updateRoleSubmitLabel": "Update Role",
"transferOwnership": "Transfer Ownership",
"transferOwnershipDescription": "Transfer ownership of the company account to another member.",
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary admin of the company account.",
"deleteInvitation": "Delete Invitation",
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
"deleteInviteSuccessMessage": "Invite deleted successfully",
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
"transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to <b>{{ member }}</b>.",
"transferringOwnership": "Transferring ownership...",
"transferOwnershipSuccess": "Ownership successfully transferred",
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
"deleteInviteSubmitLabel": "Delete Invite",
"youBadgeLabel": "You",
"updateTeamLoadingMessage": "Updating Company...",
"updateTeamSuccessMessage": "Company successfully updated",
"updateTeamErrorMessage": "Could not update Company. Please try again.",
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
"teamNameInputLabel": "Company Name",
"teamLogoInputHeading": "Upload your company's Logo",
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
"updateTeamSubmitLabel": "Update Company",
"inviteMembersHeading": "Invite Members to your Company",
"inviteMembersDescription": "Invite member to your company by entering their email and role.",
"benefitStatistics": {
"budget": {
"title": "Баланс Tervisekassa компании",
"balance": "Остаток бюджета {{balance}}",
"volume": "Объем бюджета {{volume}}"
},
"data": {
"reservations": "{{value}} услуги",
"analysis": "Анализы",
"doctorsAndSpecialists": "Врачи и специалисты",
"researches": "Исследования",
"healthResearchPlans": "Пакеты медицинских исследований",
"serviceUsage": "{{value}} использование услуг",
"serviceSum": "Сумма услуг",
"eclinic": "Дигиклиника"
}
},
"healthDetails": {
"women": "Женщины",
"men": "Мужчины",
"avgAge": "Средний возраст",
"bmi": "ИМТ",
"cholesterol": "Общий холестерин",
"vitaminD": "Витамин D",
"smokers": "Курящие"
},
"yourTeams": "Ваши компании ({{teamsCount}})",
"createTeam": "Создать компанию",
"creatingTeam": "Создание компании...",
"personalAccount": "Личный аккаунт",
"searchAccount": "Поиск аккаунта...",
"membersTabLabel": "Сотрудники",
"memberName": "Имя",
"youLabel": "Вы",
"emailLabel": "E-mail",
"roleLabel": "Роль",
"primaryOwnerLabel": "Главный админ",
"joinedAtLabel": "Присоединился",
"invitedAtLabel": "Приглашен",
"inviteMembersPageSubheading": "Пригласите сотрудников в компанию",
"createTeamModalHeading": "Создать компанию",
"createTeamModalDescription": "Создайте новую компанию для управления проектами и сотрудниками.",
"teamNameLabel": "Название компании",
"teamNameDescription": "Название вашей компании должно быть уникальным и описательным",
"createTeamSubmitLabel": "Создать компанию",
"createTeamSuccess": "Компания успешно создана",
"createTeamError": "Компания не создана. Пожалуйста, попробуйте снова.",
"createTeamLoading": "Создание компании...",
"settingsPageLabel": "Основное",
"createTeamDropdownLabel": "Новая компания",
"changeRole": "Изменить роль",
"removeMember": "Удалить из аккаунта",
"inviteMembersSuccess": "Сотрудники успешно приглашены!",
"inviteMembersError": "Произошла ошибка! Пожалуйста, попробуйте снова",
"inviteMembersLoading": "Приглашение сотрудников...",
"removeInviteButtonLabel": "Удалить приглашение",
"addAnotherMemberButtonLabel": "Добавить ещё одного",
"inviteMembersButtonLabel": "Отправить приглашения",
"removeMemberModalHeading": "Вы удаляете этого пользователя",
"removeMemberModalDescription": "Удалите этого сотрудника из компании. Он больше не будет иметь доступ к компании.",
"removeMemberSuccessMessage": "Сотрудник успешно удален",
"removeMemberErrorMessage": "Произошла ошибка. Пожалуйста, попробуйте снова",
"removeMemberErrorHeading": "Не удалось удалить выбранного сотрудника.",
"removeMemberLoadingMessage": "Удаление сотрудника...",
"removeMemberSubmitLabel": "Удалить пользователя из компании",
"chooseDifferentRoleError": "Роль совпадает с текущей",
"updateRole": "Обновить роль",
"updateRoleLoadingMessage": "Обновление роли...",
"updateRoleSuccessMessage": "Роль успешно обновлена",
"updatingRoleErrorMessage": "Произошла ошибка. Пожалуйста, попробуйте снова.",
"updateMemberRoleModalHeading": "Обновить роль сотрудника",
"updateMemberRoleModalDescription": "Измените роль выбранного сотрудника. Роль определяет его права.",
"roleMustBeDifferent": "Роль должна отличаться от текущей",
"memberRoleInputLabel": "Роль сотрудника",
"updateRoleDescription": "Выберите роль для этого сотрудника.",
"updateRoleSubmitLabel": "Обновить роль",
"transferOwnership": "Передача собственности",
"transferOwnershipDescription": "Передайте права владельца аккаунта компании другому сотруднику.",
"transferOwnershipInputLabel": "Пожалуйста, введите TRANSFER для подтверждения передачи собственности.",
"transferOwnershipInputDescription": "Передав права собственности, вы больше не будете главным администратором компании.",
"deleteInvitation": "Удалить приглашение",
"deleteInvitationDialogDescription": "Вы собираетесь удалить приглашение. Пользователь больше не сможет присоединиться к аккаунту компании.",
"deleteInviteSuccessMessage": "Приглашение успешно удалено",
"deleteInviteErrorMessage": "Приглашение не удалено. Пожалуйста, попробуйте снова.",
"deleteInviteLoadingMessage": "Удаление приглашения...",
"confirmDeletingMemberInvite": "Вы удаляете приглашение для <b>{{ email }}</b>",
"transferOwnershipDisclaimer": "Вы передаете права собственности выбранного аккаунта компании <b>{{ member }}</b>.",
"transferringOwnership": "Передача собственности...",
"transferOwnershipSuccess": "Права собственности успешно переданы",
"transferOwnershipError": "Не удалось передать права собственности выбранному сотруднику. Пожалуйста, попробуйте снова.",
"deleteInviteSubmitLabel": "Удалить приглашение",
"youBadgeLabel": "Вы",
"updateTeamLoadingMessage": "Обновление компании...",
"updateTeamSuccessMessage": "Компания успешно обновлена",
"updateTeamErrorMessage": "Не удалось обновить компанию. Пожалуйста, попробуйте снова.",
"updateLogoErrorMessage": "Не удалось обновить логотип. Пожалуйста, попробуйте снова.",
"teamNameInputLabel": "Название компании",
"teamLogoInputHeading": "Загрузите логотип вашей компании",
"teamLogoInputSubheading": "Выберите фото для загрузки в качестве логотипа компании.",
"updateTeamSubmitLabel": "Обновить компанию",
"inviteMembersHeading": "Пригласить сотрудников в компанию",
"inviteMembersDescription": "Пригласите сотрудника в компанию, указав его email и роль.",
"emailPlaceholder": "member@email.com",
"membersPageHeading": "Members",
"inviteMembersButton": "Invite Members",
"invitingMembers": "Inviting members...",
"inviteMembersSuccessMessage": "Members invited successfully",
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
"pendingInvitesHeading": "Pending Invites",
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
"noPendingInvites": "No pending invites found",
"loadingMembers": "Loading members...",
"loadMembersError": "Sorry, we couldn't fetch your company's members.",
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited members.",
"loadingInvitedMembers": "Loading invited members...",
"invitedBadge": "Invited",
"duplicateInviteEmailError": "You have already entered this email address",
"invitingOwnAccountError": "Hey, that's your email!",
"dangerZone": "Danger Zone",
"dangerZoneSubheading": "Delete or leave your company",
"deleteTeam": "Delete Company",
"deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.",
"deletingTeam": "Deleting company",
"deleteTeamModalHeading": "Deleting Company",
"deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.",
"deleteTeamInputField": "Type the name of the company to confirm",
"leaveTeam": "Leave Company",
"leavingTeamModalHeading": "Leaving Company",
"leavingTeamModalDescription": "You are about to leave this company. You will no longer have access to it.",
"leaveTeamDescription": "Click the button below to leave the company. Remember, you will no longer have access to it and will need to be re-invited to join.",
"deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.",
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
"searchMembersPlaceholder": "Search members",
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
"searchInvitations": "Search Invitations",
"updateInvitation": "Update Invitation",
"removeInvitation": "Remove Invitation",
"acceptInvitation": "Accept Invitation",
"renewInvitation": "Renew Invitation",
"resendInvitation": "Resend Invitation",
"expiresAtLabel": "Expires at",
"expired": "Expired",
"active": "Active",
"inviteStatus": "Status",
"inviteNotFoundOrExpired": "Invite not found or expired",
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company admin to renew the invite.",
"backToHome": "Back to Home",
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
"signInWithDifferentAccount": "Sign in with a different account",
"signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.",
"acceptInvitationHeading": "Accept Invitation to join {{accountName}}",
"acceptInvitationDescription": "You have been invited to join the company {{accountName}}. If you wish to accept the invitation, please click the button below.",
"continueAs": "Continue as {{email}}",
"joinTeamAccount": "Join Company",
"joiningTeam": "Joining company...",
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
"reservedNameError": "This name is reserved. Please choose a different one.",
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.",
"personalCode": "Личный код"
"membersPageHeading": "Сотрудники",
"inviteMembersButton": "Пригласить сотрудников",
"invitingMembers": "Приглашение сотрудников...",
"inviteMembersSuccessMessage": "Сотрудники успешно приглашены",
"inviteMembersErrorMessage": "Не удалось пригласить сотрудников. Пожалуйста, попробуйте снова.",
"pendingInvitesHeading": "Ожидающие приглашения",
"pendingInvitesDescription": "Здесь можно управлять ожидающими приглашениями в вашу компанию.",
"noPendingInvites": "Нет ожидающих приглашений",
"loadingMembers": "Загрузка сотрудников...",
"loadMembersError": "Не удалось получить список сотрудников компании.",
"loadInvitedMembersError": "Не удалось получить список приглашённых сотрудников компании.",
"loadingInvitedMembers": "Загрузка приглашённых сотрудников...",
"invitedBadge": "Приглашён",
"duplicateInviteEmailError": "Вы уже добавили этот адрес электронной почты",
"invitingOwnAccountError": "Эй, это ваш email!",
"dangerZone": "Опасная зона",
"dangerZoneSubheading": "Удалить или покинуть компанию",
"deleteTeam": "Удалить компанию",
"deleteTeamDescription": "Это действие невозможно отменить. Все данные, связанные с этой компанией, будут удалены.",
"deletingTeam": "Удаление компании...",
"deleteTeamModalHeading": "Удаление компании",
"deletingTeamDescription": "Вы собираетесь удалить компанию {{ teamName }}. Это действие невозможно отменить.",
"deleteTeamInputField": "Введите название компании для подтверждения",
"leaveTeam": "Покинуть компанию",
"leavingTeamModalHeading": "Покидание компании",
"leavingTeamModalDescription": "Вы собираетесь покинуть эту компанию. У вас больше не будет к ней доступа.",
"leaveTeamDescription": "Нажмите кнопку ниже, чтобы покинуть компанию. Помните, что вы больше не будете иметь к ней доступа и вам потребуется повторное приглашение для присоединения.",
"deleteTeamDisclaimer": "Вы удаляете компанию {{ teamName }}. Это действие невозможно отменить.",
"leaveTeamDisclaimer": "Вы покидаете компанию {{ teamName }}. У вас больше не будет к ней доступа.",
"deleteTeamErrorHeading": "Не удалось удалить вашу компанию.",
"leaveTeamErrorHeading": "Не удалось покинуть вашу компанию.",
"searchMembersPlaceholder": "Поиск сотрудников",
"createTeamErrorHeading": "Не удалось создать вашу компанию.",
"createTeamErrorMessage": "Произошла ошибка при создании компании. Пожалуйста, попробуйте снова.",
"transferTeamErrorHeading": "Не удалось передать права собственности аккаунта компании.",
"transferTeamErrorMessage": "Произошла ошибка при передаче прав собственности аккаунта компании. Пожалуйста, попробуйте снова.",
"updateRoleErrorHeading": "Не удалось обновить роль выбранного сотрудника.",
"updateRoleErrorMessage": "Произошла ошибка при обновлении роли выбранного сотрудника. Пожалуйста, попробуйте снова.",
"searchInvitations": "Поиск приглашений",
"updateInvitation": "Обновить приглашение",
"removeInvitation": "Удалить приглашение",
"acceptInvitation": "Принять приглашение",
"renewInvitation": "Продлить приглашение",
"resendInvitation": "Отправить приглашение повторно",
"expiresAtLabel": "Истекает",
"expired": "Истекло",
"active": "Активно",
"inviteStatus": "Статус",
"inviteNotFoundOrExpired": "Приглашение не найдено или истекло",
"inviteNotFoundOrExpiredDescription": "Приглашение, которое вы ищете, либо истекло, либо не существует. Пожалуйста, свяжитесь с администратором компании для продления приглашения.",
"backToHome": "Назад на главную",
"renewInvitationDialogDescription": "Вы собираетесь продлить приглашение для {{ email }}. Пользователь сможет присоединиться к компании.",
"renewInvitationErrorTitle": "Не удалось продлить приглашение.",
"renewInvitationErrorDescription": "Произошла ошибка при продлении приглашения. Пожалуйста, попробуйте снова.",
"signInWithDifferentAccount": "Войти с другим аккаунтом",
"signInWithDifferentAccountDescription": "Если вы хотите принять приглашение с другим аккаунтом, выйдите из системы и войдите с нужным аккаунтом.",
"acceptInvitationHeading": "Принять приглашение для присоединения к {{accountName}}",
"acceptInvitationDescription": "Вас пригласили присоединиться к компании {{accountName}}. Чтобы принять приглашение, нажмите кнопку ниже.",
"continueAs": "Продолжить как {{email}}",
"joinTeamAccount": "Присоединиться к компании",
"joiningTeam": "Присоединение к компании...",
"leaveTeamInputLabel": "Пожалуйста, введите LEAVE для подтверждения выхода из компании.",
"leaveTeamInputDescription": "При выходе из компании у вас больше не будет к ней доступа.",
"reservedNameError": "Это имя зарезервировано. Пожалуйста, выберите другое.",
"specialCharactersError": "Это имя не может содержать специальные символы. Пожалуйста, выберите другое.",
"personalCode": "Идентификационный код",
"teamOwnerPersonalCodeLabel": "Идентификационный код владельца"
}

View File

@@ -135,6 +135,7 @@
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--breakpoint-2xs: 36rem;
--breakpoint-xs: 48rem;
--breakpoint-sm: 64rem;
--breakpoint-md: 70rem;

View File

@@ -0,0 +1,17 @@
create extension if not exists pg_cron;
create extension if not exists pg_net;
select
cron.schedule(
'send emails with new unassigned jobs 4x a day',
'0 4,9,14,18 * * 1-5', -- Run at 07:00, 12:00, 17:00 and 21:00 (GMT +3) on weekdays only
$$
select
net.http_post(
url := 'https://test.medreport.ee/api/job/send-open-jobs-emails',
headers := jsonb_build_object(
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
)
) as request_id;
$$
);

View File

@@ -0,0 +1,8 @@
grant select on table "medreport"."doctor_analysis_feedback" to "service_role";
create policy "service_role_select"
on "medreport"."doctor_analysis_feedback"
as permissive
for select
to service_role
using (true);

View File

@@ -0,0 +1,10 @@
create trigger "trigger_doctor_notification" after update
on "medreport"."analysis_orders" for each row
execute function "supabase_functions"."http_request"(
'http://host.docker.internal:3000/api/db/webhook',
'POST',
'{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}',
'{}',
'5000'
);

View File

@@ -0,0 +1,10 @@
alter table audit.notification_entries enable row level security;
create policy "service_role_insert"
on "audit"."notification_entries"
as permissive
for insert
to service_role
with check (true);
grant insert on table "audit"."notification_entries" to "service_role";