Merge branch 'develop'
This commit is contained in:
15
.env.staging
Normal file
15
.env.staging
Normal 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
|
||||
11
Dockerfile
11
Dockerfile
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
23
app/api/job/handler/send-open-jobs-emails.ts
Normal file
23
app/api/job/handler/send-open-jobs-emails.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
53
app/api/job/send-open-jobs-emails/route.ts
Normal file
53
app/api/job/send-open-jobs-emails/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
107
app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx
Normal file
107
app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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()} />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
22
app/home/(user)/_lib/server/load-user-analyses.ts
Normal file
22
app/home/(user)/_lib/server/load-user-analyses.ts
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
32
lib/services/doctor-jobs.service.ts
Normal file
32
lib/services/doctor-jobs.service.ts
Normal 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) || [];
|
||||
}
|
||||
@@ -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({
|
||||
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: process.env.CONTACT_EMAIL || '',
|
||||
});
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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:"
|
||||
}
|
||||
@@ -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,"
|
||||
}
|
||||
@@ -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,"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>"
|
||||
}
|
||||
}
|
||||
@@ -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": "Телефон:"
|
||||
}
|
||||
@@ -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": "Если кнопка не работает, скопируйте эту ссылку в ваш браузер:"
|
||||
}
|
||||
@@ -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,"
|
||||
}
|
||||
@@ -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,"
|
||||
}
|
||||
@@ -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>"
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './use-csrf-token';
|
||||
export * from './use-current-locale-language-names';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -12,5 +12,6 @@
|
||||
"normal": "Normal range"
|
||||
}
|
||||
},
|
||||
"orderTitle": "Order number {{orderNumber}}"
|
||||
"orderTitle": "Order number {{orderNumber}}",
|
||||
"view": "View results"
|
||||
}
|
||||
@@ -116,5 +116,6 @@
|
||||
"confirm": "Confirm",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"invalidDataError": "Invalid data submitted"
|
||||
"invalidDataError": "Invalid data submitted",
|
||||
"language": "Language"
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"assignedTo": "Doctor",
|
||||
"resultsStatus": "Analysis results",
|
||||
"waitingForNr": "Waiting for {{nr}}",
|
||||
"language": "Preferred language",
|
||||
"responsesReceived": "Results complete"
|
||||
},
|
||||
"otherPatients": "Other patients",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -12,5 +12,6 @@
|
||||
"normal": "Normaalne vahemik"
|
||||
}
|
||||
},
|
||||
"orderTitle": "Tellimus {{orderNumber}}"
|
||||
"orderTitle": "Tellimus {{orderNumber}}",
|
||||
"view": "Vaata tulemusi"
|
||||
}
|
||||
@@ -136,5 +136,6 @@
|
||||
"confirm": "Kinnita",
|
||||
"previous": "Eelmine",
|
||||
"next": "Järgmine",
|
||||
"invalidDataError": "Vigased andmed"
|
||||
"invalidDataError": "Vigased andmed",
|
||||
"language": "Keel"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"assignedTo": "Arst",
|
||||
"resultsStatus": "Analüüsitulemused",
|
||||
"waitingForNr": "Ootel {{nr}}",
|
||||
"language": "Patsiendi keel",
|
||||
"responsesReceived": "Tulemused koos"
|
||||
},
|
||||
"otherPatients": "Muud patsiendid",
|
||||
|
||||
@@ -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": "Обновление предпочитаемого языка..."
|
||||
}
|
||||
16
public/locales/ru/analysis-results.json
Normal file
16
public/locales/ru/analysis-results.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"pageTitle": "Мои результаты анализов",
|
||||
"description": "Все результаты анализов появляются в течение 1-3 рабочих дней после их сдачи.",
|
||||
"descriptionEmpty": "Если вы уже сдали анализы, то вскоре здесь появятся ваши результаты.",
|
||||
"orderNewAnalysis": "Заказать новые анализы",
|
||||
"waitingForResults": "Ожидание результатов",
|
||||
"noAnalysisElements": "Анализы еще не заказаны",
|
||||
"noAnalysisOrders": "Пока нет заказов на анализы",
|
||||
"analysisDate": "Дата результата анализа",
|
||||
"results": {
|
||||
"range": {
|
||||
"normal": "Нормальный диапазон"
|
||||
}
|
||||
},
|
||||
"orderTitle": "Заказ {{orderNumber}}"
|
||||
}
|
||||
@@ -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": "Пароль не может совпадать с текущим паролем"
|
||||
}
|
||||
}
|
||||
@@ -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": "Итого"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"title": "Select service",
|
||||
"description": "Select the appropriate service or package according to your health needs or goals.",
|
||||
"title": "Выберите услугу",
|
||||
"description": "Выберите подходящую услугу или пакет в зависимости от вашей проблемы со здоровьем или цели.",
|
||||
"analysisPackages": {
|
||||
"title": "Analysis packages",
|
||||
"description": "Get to know the personal analysis packages and order"
|
||||
}
|
||||
"title": "Пакеты анализов",
|
||||
"description": "Ознакомьтесь с персональными пакетами анализов и закажите"
|
||||
},
|
||||
"noCategories": "Список услуг не найден, попробуйте позже"
|
||||
}
|
||||
75
public/locales/ru/cart.json
Normal file
75
public/locales/ru/cart.json
Normal 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": "Выберите местоположение"
|
||||
}
|
||||
}
|
||||
@@ -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": "Некорректные данные"
|
||||
}
|
||||
@@ -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": "Закажите подходящий для вас анализ"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 символов"
|
||||
}
|
||||
@@ -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": "Не хочу проходить аудит здоровья в данный момент"
|
||||
}
|
||||
@@ -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": "Не удалось добавить пакет анализов в корзину"
|
||||
}
|
||||
7
public/locales/ru/order-analysis.json
Normal file
7
public/locales/ru/order-analysis.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Выберите анализ",
|
||||
"description": "Результаты всех анализов будут доступны в течение 1–3 рабочих дней после сдачи крови.",
|
||||
"analysisNotAvailable": "Заказ анализа в данный момент недоступен",
|
||||
"analysisAddedToCart": "Анализ добавлен в корзину",
|
||||
"analysisAddToCartError": "Не удалось добавить анализ в корзину"
|
||||
}
|
||||
19
public/locales/ru/orders.json
Normal file
19
public/locales/ru/orders.json
Normal 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": "Отменено"
|
||||
}
|
||||
}
|
||||
@@ -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": "Ожидается"
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"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": "Личный код"
|
||||
"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": "Сотрудники",
|
||||
"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": "Идентификационный код владельца"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
$$
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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'
|
||||
);
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user