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 tooling tooling
|
||||||
COPY .env .env
|
COPY .env .env
|
||||||
COPY .env.production .env.production
|
COPY .env.production .env.production
|
||||||
|
COPY .env.staging .env.staging
|
||||||
|
|
||||||
# Load env file and echo a specific variable
|
# Load env file and echo a specific variable
|
||||||
# RUN dotenv -e .env -- printenv | grep 'SUPABASE' || true
|
# RUN dotenv -e .env -- printenv | grep 'SUPABASE' || true
|
||||||
@@ -20,13 +21,10 @@ COPY . .
|
|||||||
|
|
||||||
ENV NODE_ENV=production
|
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 \
|
RUN set -a \
|
||||||
&& . .env \
|
&& . .env \
|
||||||
&& . .env.production \
|
&& . .env.production \
|
||||||
|
&& . .env.staging \
|
||||||
&& set +a \
|
&& set +a \
|
||||||
&& node check-env.js \
|
&& node check-env.js \
|
||||||
&& pnpm build
|
&& pnpm build
|
||||||
@@ -34,18 +32,21 @@ RUN set -a \
|
|||||||
|
|
||||||
# --- Stage 2: Runtime ---
|
# --- Stage 2: Runtime ---
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
ARG APP_ENV=production
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app ./
|
COPY --from=builder /app ./
|
||||||
|
|
||||||
|
RUN cp ".env.${APP_ENV}" .env.local
|
||||||
|
|
||||||
RUN npm install -g pnpm@9 \
|
RUN npm install -g pnpm@9 \
|
||||||
&& pnpm install --prod --frozen-lockfile
|
&& pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# 🔍 Optional: Log key envs for debug
|
# 🔍 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
|
&& 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>
|
||||||
|
|
||||||
<div className={'flex gap-x-2.5'}>
|
<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}>
|
<Link href={pathsConfig.auth.signIn}>
|
||||||
<Trans i18nKey={'auth:signIn'} />
|
<Trans i18nKey={'auth:signIn'} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { SubmitButton } from '@kit/shared/components/ui/submit-button';
|
import { sendEmailFromTemplate } from '@/lib/services/mailer.service';
|
||||||
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
|
|
||||||
import { CompanySubmitData } from '@/lib/types/company';
|
import { CompanySubmitData } from '@/lib/types/company';
|
||||||
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
|
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { FormItem } from '@kit/ui/form';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Label } from '@kit/ui/label';
|
import { Label } from '@kit/ui/label';
|
||||||
@@ -39,7 +40,14 @@ const CompanyOfferForm = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendCompanyOfferEmail(data, language)
|
sendEmailFromTemplate(
|
||||||
|
renderCompanyOfferEmail,
|
||||||
|
{
|
||||||
|
companyData: data,
|
||||||
|
language,
|
||||||
|
},
|
||||||
|
process.env.CONTACT_EMAIL!,
|
||||||
|
)
|
||||||
.then(() => router.push('/company-offer/success'))
|
.then(() => router.push('/company-offer/success'))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setIsLoading(false);
|
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');
|
console.info('Inserting sync entry');
|
||||||
await createSyncSuccessEntry();
|
await createSyncSuccessEntry();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await createSyncFailEntry(JSON.stringify(e));
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
console.error(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(
|
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 { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { capitalize } from 'lodash';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
|
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
|
||||||
@@ -22,6 +23,9 @@ import {
|
|||||||
doctorAnalysisFeedbackFormSchema,
|
doctorAnalysisFeedbackFormSchema,
|
||||||
} from '@kit/doctor/schema/doctor-analysis.schema';
|
} from '@kit/doctor/schema/doctor-analysis.schema';
|
||||||
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
||||||
|
import {
|
||||||
|
useCurrentLocaleLanguageNames
|
||||||
|
} from '@kit/shared/hooks';
|
||||||
import { getFullName } from '@kit/shared/utils';
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -57,6 +61,8 @@ export default function AnalysisView({
|
|||||||
|
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
|
||||||
|
const languageNames = useCurrentLocaleLanguageNames();
|
||||||
|
|
||||||
const isInProgress = !!(
|
const isInProgress = !!(
|
||||||
!!feedback?.status &&
|
!!feedback?.status &&
|
||||||
feedback?.doctor_user_id &&
|
feedback?.doctor_user_id &&
|
||||||
@@ -191,6 +197,12 @@ export default function AnalysisView({
|
|||||||
<Trans i18nKey="doctor:email" />
|
<Trans i18nKey="doctor:email" />
|
||||||
</div>
|
</div>
|
||||||
<div>{patient.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>
|
||||||
<div className="xs:hidden block">
|
<div className="xs:hidden block">
|
||||||
<DoctorJobSelect
|
<DoctorJobSelect
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Eye } from 'lucide-react';
|
|||||||
import { getResultSetName } from '@kit/doctor/lib/helpers';
|
import { getResultSetName } from '@kit/doctor/lib/helpers';
|
||||||
import { ResponseTable } from '@kit/doctor/schema/doctor-analysis.schema';
|
import { ResponseTable } from '@kit/doctor/schema/doctor-analysis.schema';
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
|
||||||
import { getFullName } from '@kit/shared/utils';
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -23,7 +24,9 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@kit/ui/table';
|
} from '@kit/ui/table';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import DoctorJobSelect from './doctor-job-select';
|
import DoctorJobSelect from './doctor-job-select';
|
||||||
|
import { capitalize } from 'lodash';
|
||||||
|
|
||||||
export default function ResultsTable({
|
export default function ResultsTable({
|
||||||
results = [],
|
results = [],
|
||||||
@@ -58,6 +61,8 @@ export default function ResultsTable({
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
|
const languageNames = useCurrentLocaleLanguageNames();
|
||||||
|
|
||||||
const fetchPage = async (page: number) => {
|
const fetchPage = async (page: number) => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const result = await fetchAction({
|
const result = await fetchAction({
|
||||||
@@ -116,6 +121,9 @@ export default function ResultsTable({
|
|||||||
<TableHead className="w-20">
|
<TableHead className="w-20">
|
||||||
<Trans i18nKey="doctor:resultsTable.resultsStatus" />
|
<Trans i18nKey="doctor:resultsTable.resultsStatus" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
<TableHead className="w-20">
|
||||||
|
<Trans i18nKey="doctor:resultsTable.language" />
|
||||||
|
</TableHead>
|
||||||
<TableHead className="w-20">
|
<TableHead className="w-20">
|
||||||
<Trans i18nKey="doctor:resultsTable.assignedTo" />
|
<Trans i18nKey="doctor:resultsTable.assignedTo" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -179,6 +187,11 @@ export default function ResultsTable({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{capitalize(
|
||||||
|
languageNames.of(result?.patient?.preferred_locale ?? 'et'),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DoctorJobSelect
|
<DoctorJobSelect
|
||||||
doctorUserId={result.doctor?.primary_owner_user_id}
|
doctorUserId={result.doctor?.primary_owner_user_id}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ async function AnalysisPage({
|
|||||||
id: string;
|
id: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const { id: analysisResponseId } = await params;
|
const { id: analysisOrderId } = await params;
|
||||||
const analysisResultDetails = await loadResult(Number(analysisResponseId));
|
const analysisResultDetails = await loadResult(Number(analysisOrderId));
|
||||||
|
|
||||||
if (!analysisResultDetails) {
|
if (!analysisResultDetails) {
|
||||||
return null;
|
return null;
|
||||||
@@ -28,7 +28,7 @@ async function AnalysisPage({
|
|||||||
if (analysisResultDetails) {
|
if (analysisResultDetails) {
|
||||||
await createDoctorPageViewLog({
|
await createDoctorPageViewLog({
|
||||||
action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
|
action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||||
recordKey: analysisResponseId,
|
recordKey: analysisOrderId,
|
||||||
dataOwnerUserId: analysisResultDetails.patient.userId,
|
dataOwnerUserId: analysisResultDetails.patient.userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -50,3 +50,5 @@ async function AnalysisPage({
|
|||||||
|
|
||||||
export default DoctorGuard(AnalysisPage);
|
export default DoctorGuard(AnalysisPage);
|
||||||
const loadResult = cache(getAnalysisResultsForDoctor);
|
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 Link from 'next/link';
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import { ChevronRight, HeartPulse } from 'lucide-react';
|
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() {
|
export default function DashboardCards() {
|
||||||
return (
|
return (
|
||||||
<div className='flex gap-4 lg:px-4'>
|
<div className="flex gap-4 lg:px-4">
|
||||||
<Card
|
<Card
|
||||||
variant="gradient-success"
|
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">
|
<CardHeader className="flex-row">
|
||||||
<div
|
<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" />
|
<HeartPulse className="size-4 fill-green-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
<div className="bg-warning ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white">
|
||||||
<Link href='/home/order-analysis'>
|
<Link href="/home/order-analysis">
|
||||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||||
<ChevronRight className="size-4 stroke-2" />
|
<ChevronRight className="size-4 stroke-2" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -33,10 +31,10 @@ export default function DashboardCards() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex flex-col items-start gap-2">
|
<CardFooter className="flex flex-col items-start gap-2">
|
||||||
<h5>
|
<h5>
|
||||||
<Trans i18nKey='dashboard:heroCard.orderAnalysis.title' />
|
<Trans i18nKey="dashboard:heroCard.orderAnalysis.title" />
|
||||||
</h5>
|
</h5>
|
||||||
<CardDescription className="text-primary">
|
<CardDescription className="text-primary">
|
||||||
<Trans i18nKey='dashboard:heroCard.orderAnalysis.description' />
|
<Trans i18nKey="dashboard:heroCard.orderAnalysis.description" />
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default function Dashboard({
|
|||||||
|
|
||||||
return (
|
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({
|
{cards({
|
||||||
gender: params?.gender,
|
gender: params?.gender,
|
||||||
age: params?.age,
|
age: params?.age,
|
||||||
@@ -233,8 +233,11 @@ export default function Dashboard({
|
|||||||
index,
|
index,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between" key={index}>
|
<div
|
||||||
<div className="mr-4 flex flex-row items-center gap-4">
|
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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
||||||
@@ -243,7 +246,7 @@ export default function Dashboard({
|
|||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-fit">
|
||||||
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
|
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
|
||||||
{title}
|
{title}
|
||||||
<InfoTooltip content={tooltipContent} />
|
<InfoTooltip content={tooltipContent} />
|
||||||
@@ -253,16 +256,24 @@ export default function Dashboard({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4">
|
<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>
|
<p className="text-sm font-medium"> {price}</p>
|
||||||
{href ? (
|
{href ? (
|
||||||
<Link href={href}>
|
<Link href={href}>
|
||||||
<Button size="sm" variant="secondary">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full min-w-fit"
|
||||||
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Button size="sm" variant="secondary">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full min-w-fit"
|
||||||
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { StoreCart } from '@medusajs/types';
|
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 {
|
import {
|
||||||
featureFlagsConfig,
|
pathsConfig,
|
||||||
personalAccountNavigationConfig,
|
personalAccountNavigationConfig,
|
||||||
} from '@kit/shared/config';
|
} from '@kit/shared/config';
|
||||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||||
@@ -15,7 +19,6 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@kit/ui/dropdown-menu';
|
} from '@kit/ui/dropdown-menu';
|
||||||
@@ -23,14 +26,16 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
// home imports
|
// home imports
|
||||||
import { HomeAccountSelector } from '../_components/home-account-selector';
|
|
||||||
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
||||||
|
|
||||||
export function HomeMobileNavigation(props: {
|
export function HomeMobileNavigation(props: {
|
||||||
workspace: UserWorkspace;
|
workspace: UserWorkspace;
|
||||||
cart: StoreCart | null;
|
cart: StoreCart | null;
|
||||||
}) {
|
}) {
|
||||||
|
const user = props.workspace.user;
|
||||||
|
|
||||||
const signOut = useSignOut();
|
const signOut = useSignOut();
|
||||||
|
const { data: personalAccountData } = usePersonalAccountData(user.id);
|
||||||
|
|
||||||
const Links = personalAccountNavigationConfig.routes.map((item, index) => {
|
const Links = personalAccountNavigationConfig.routes.map((item, index) => {
|
||||||
if ('children' in item) {
|
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;
|
const hasCartItems = cartQuantityTotal > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,22 +88,6 @@ export function HomeMobileNavigation(props: {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
|
<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}>
|
<If condition={props.cart && hasCartItems}>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownLink
|
<DropdownLink
|
||||||
@@ -91,6 +102,41 @@ export function HomeMobileNavigation(props: {
|
|||||||
|
|
||||||
<DropdownMenuGroup>{Links}</DropdownMenuGroup>
|
<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 />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<SignOutDropdownItem onSignOut={() => signOut.mutateAsync()} />
|
<SignOutDropdownItem onSignOut={() => signOut.mutateAsync()} />
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function OrderAnalysesCards({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
<div className="grid 2xs:grid-cols-3 gap-6 mt-4">
|
||||||
{analyses.map(({
|
{analyses.map(({
|
||||||
title,
|
title,
|
||||||
variant,
|
variant,
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
'use client';
|
'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 {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableHeader,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
} from '@kit/ui/table';
|
} from '@kit/ui/table';
|
||||||
import { StoreOrderLineItem } from "@medusajs/types";
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { AnalysisOrder } from '~/lib/services/order.service';
|
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';
|
import { logAnalysisResultsNavigateAction } from './actions';
|
||||||
|
|
||||||
export default function OrderItemsTable({ items, title, analysisOrder }: {
|
export default function OrderItemsTable({
|
||||||
|
items,
|
||||||
|
title,
|
||||||
|
analysisOrder,
|
||||||
|
}: {
|
||||||
items: StoreOrderLineItem[];
|
items: StoreOrderLineItem[];
|
||||||
title: string;
|
title: string;
|
||||||
analysisOrder: AnalysisOrder;
|
analysisOrder: AnalysisOrder;
|
||||||
@@ -29,11 +39,11 @@ export default function OrderItemsTable({ items, title, analysisOrder }: {
|
|||||||
|
|
||||||
const openAnalysisResults = async () => {
|
const openAnalysisResults = async () => {
|
||||||
await logAnalysisResultsNavigateAction(analysisOrder.medusa_order_id);
|
await logAnalysisResultsNavigateAction(analysisOrder.medusa_order_id);
|
||||||
router.push(`/home/analysis-results`);
|
router.push(`${pathsConfig.app.analysisResults}/${analysisOrder.id}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="rounded-lg border border-separate">
|
<Table className="border-separate rounded-lg border">
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-6">
|
||||||
@@ -45,13 +55,14 @@ export default function OrderItemsTable({ items, title, analysisOrder }: {
|
|||||||
<TableHead className="px-6">
|
<TableHead className="px-6">
|
||||||
<Trans i18nKey="orders:table.status" />
|
<Trans i18nKey="orders:table.status" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-6"></TableHead>
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items
|
{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) => (
|
.map((orderItem) => (
|
||||||
<TableRow className="w-full" key={orderItem.id}>
|
<TableRow className="w-full" key={orderItem.id}>
|
||||||
<TableCell className="text-left w-[100%] px-6">
|
<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')}
|
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-6 min-w-[180px]">
|
<TableCell className="min-w-[180px] px-6">
|
||||||
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
<Trans i18nKey={`orders:status.${analysisOrder.status}`} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right px-6">
|
<TableCell className="px-6 text-right">
|
||||||
<span className="flex gap-x-1 justify-end w-[30px]">
|
<Button size="sm" onClick={openAnalysisResults}>
|
||||||
<button
|
<Trans i18nKey="analysis-results:view" />
|
||||||
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer "
|
</Button>
|
||||||
onClick={openAnalysisResults}
|
|
||||||
>
|
|
||||||
<Eye />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { listRegions } from '@lib/data/regions';
|
|||||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||||
import type { StoreProduct } from '@medusajs/types';
|
import type { StoreProduct } from '@medusajs/types';
|
||||||
import { loadCurrentUserAccount } from './load-user-account';
|
import { loadCurrentUserAccount } from './load-user-account';
|
||||||
import { AnalysisPackageWithVariant } from '~/components/select-analysis-package';
|
|
||||||
import { AccountWithParams } from '@/packages/features/accounts/src/server/api';
|
import { AccountWithParams } from '@/packages/features/accounts/src/server/api';
|
||||||
|
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||||
|
|
||||||
async function countryCodesLoader() {
|
async function countryCodesLoader() {
|
||||||
const countryCodes = await listRegions().then((regions) =>
|
const countryCodes = await listRegions().then((regions) =>
|
||||||
@@ -38,8 +38,11 @@ function userSpecificVariantLoader({
|
|||||||
if (age >= 18 && age <= 29) {
|
if (age >= 18 && age <= 29) {
|
||||||
return '18-29';
|
return '18-29';
|
||||||
}
|
}
|
||||||
if (age >= 30 && age <= 49) {
|
if (age >= 30 && age <= 39) {
|
||||||
return '30-49';
|
return '30-39';
|
||||||
|
}
|
||||||
|
if (age >= 40 && age <= 49) {
|
||||||
|
return '40-49';
|
||||||
}
|
}
|
||||||
if (age >= 50 && age <= 59) {
|
if (age >= 50 && age <= 59) {
|
||||||
return '50-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 { cache } from 'react';
|
||||||
|
|
||||||
import { createAccountsApi } from '@kit/accounts/api';
|
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';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
|
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
|
||||||
@@ -9,14 +9,15 @@ export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;
|
|||||||
/**
|
/**
|
||||||
* @name loadUserAnalysis
|
* @name loadUserAnalysis
|
||||||
* @description
|
* @description
|
||||||
* Load the user's analyses. It's a cached per-request function that fetches 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.
|
||||||
* It can be used across the server components to load the user workspace data.
|
|
||||||
*/
|
*/
|
||||||
export const loadUserAnalysis = cache(analysisLoader);
|
export const loadUserAnalysis = cache(analysisLoader);
|
||||||
|
|
||||||
async function analysisLoader(): Promise<UserAnalysis | null> {
|
async function analysisLoader(
|
||||||
|
analysisOrderId: number,
|
||||||
|
): Promise<AnalysisResultDetails | null> {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const api = createAccountsApi(client);
|
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;
|
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 { Database } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
|
|
||||||
export enum NotificationAction {
|
export enum NotificationAction {
|
||||||
DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED',
|
DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED',
|
||||||
|
NEW_JOBS_ALERT = 'NEW_JOBS_ALERT',
|
||||||
|
PATIENT_RESULTS_RECEIVED_ALERT = 'PATIENT_RESULTS_RECEIVED_ALERT',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNotificationLog = async ({
|
export const createNotificationLog = async ({
|
||||||
@@ -17,7 +19,7 @@ export const createNotificationLog = async ({
|
|||||||
relatedRecordId?: string | number;
|
relatedRecordId?: string | number;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerAdminClient();
|
||||||
|
|
||||||
await supabase
|
await supabase
|
||||||
.schema('audit')
|
.schema('audit')
|
||||||
@@ -30,6 +32,6 @@ export const createNotificationLog = async ({
|
|||||||
})
|
})
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
} catch (error) {
|
} 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';
|
'use server';
|
||||||
|
|
||||||
import { CompanySubmitData } from '@/lib/types/company';
|
import { toArray } from '@/lib/utils';
|
||||||
import { emailSchema } from '@/lib/validations/email.schema';
|
|
||||||
|
|
||||||
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
|
||||||
import { getMailer } from '@kit/mailers';
|
import { getMailer } from '@kit/mailers';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
export const sendDoctorSummaryCompletedEmail = async (
|
import { emailSchema } from '~/lib/validations/email.schema';
|
||||||
language: string,
|
|
||||||
recipientName: string,
|
|
||||||
recipientEmail: string,
|
|
||||||
orderNr: string,
|
|
||||||
orderId: number,
|
|
||||||
) => {
|
|
||||||
const { html, subject } = await renderDoctorSummaryReceivedEmail({
|
|
||||||
language,
|
|
||||||
recipientName,
|
|
||||||
recipientEmail,
|
|
||||||
orderNr,
|
|
||||||
orderId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendEmail({
|
type EmailTemplate = {
|
||||||
subject,
|
html: string;
|
||||||
html,
|
subject: string;
|
||||||
to: recipientEmail,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendCompanyOfferEmail = async (
|
type EmailRenderer<T = any> = (params: T) => Promise<EmailTemplate>;
|
||||||
data: CompanySubmitData,
|
|
||||||
language: string,
|
|
||||||
) => {
|
|
||||||
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
|
|
||||||
const { html, subject } = await renderCompanyOfferEmail({
|
|
||||||
language,
|
|
||||||
companyData: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
subject,
|
||||||
html,
|
html,
|
||||||
to: process.env.CONTACT_EMAIL || '',
|
to: email,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(emailPromises);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendEmail = enhanceAction(
|
export const sendEmail = enhanceAction(
|
||||||
@@ -53,7 +44,7 @@ export const sendEmail = enhanceAction(
|
|||||||
const log = await getLogger();
|
const log = await getLogger();
|
||||||
|
|
||||||
if (!process.env.EMAIL_USER) {
|
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');
|
throw new Error('No email user configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export async function getLatestPublicMessageListItem() {
|
|||||||
Action: MedipostAction.GetPublicMessageList,
|
Action: MedipostAction.GetPublicMessageList,
|
||||||
User: USER,
|
User: USER,
|
||||||
Password: PASSWORD,
|
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
|
// 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
|
// MessageType check only for messages of certain type
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -165,6 +165,20 @@ async function doctorMiddleware(request: NextRequest, response: NextResponse) {
|
|||||||
*/
|
*/
|
||||||
function getPatterns() {
|
function getPatterns() {
|
||||||
return [
|
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/*?' }),
|
pattern: new URLPattern({ pathname: '/admin/*?' }),
|
||||||
handler: adminMiddleware,
|
handler: adminMiddleware,
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
renderAllResultsReceivedEmail,
|
||||||
|
renderFirstResultsReceivedEmail,
|
||||||
|
} from '@kit/email-templates';
|
||||||
import { Database } from '@kit/supabase/database';
|
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';
|
import { RecordChange, Tables } from '../record-change.type';
|
||||||
|
|
||||||
export function createDatabaseWebhookRouterService(
|
export function createDatabaseWebhookRouterService(
|
||||||
@@ -42,6 +55,12 @@ class DatabaseWebhookRouterService {
|
|||||||
return this.handleAccountsWebhook(payload);
|
return this.handleAccountsWebhook(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'analysis_orders': {
|
||||||
|
const payload = body as RecordChange<typeof body.table>;
|
||||||
|
|
||||||
|
return this.handleAnalysisOrdersWebhook(payload);
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -83,4 +102,69 @@ class DatabaseWebhookRouterService {
|
|||||||
return service.handleAccountDeletedWebhook(body.old_record);
|
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({
|
export async function renderDoctorSummaryReceivedEmail({
|
||||||
language,
|
language,
|
||||||
recipientEmail,
|
|
||||||
recipientName,
|
recipientName,
|
||||||
orderNr,
|
orderNr,
|
||||||
orderId,
|
analysisOrderId,
|
||||||
}: {
|
}: {
|
||||||
language?: string;
|
language?: string;
|
||||||
recipientName: string;
|
recipientName: string;
|
||||||
recipientEmail: string;
|
|
||||||
orderNr: string;
|
orderNr: string;
|
||||||
orderId: number;
|
analysisOrderId: number;
|
||||||
}) {
|
}) {
|
||||||
const namespace = 'doctor-summary-received-email';
|
const namespace = 'doctor-summary-received-email';
|
||||||
|
|
||||||
@@ -37,8 +35,6 @@ export async function renderDoctorSummaryReceivedEmail({
|
|||||||
namespace: [namespace, 'common'],
|
namespace: [namespace, 'common'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const to = recipientEmail;
|
|
||||||
|
|
||||||
const previewText = t(`${namespace}:previewText`, {
|
const previewText = t(`${namespace}:previewText`, {
|
||||||
orderNr,
|
orderNr,
|
||||||
});
|
});
|
||||||
@@ -73,13 +69,13 @@ export async function renderDoctorSummaryReceivedEmail({
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<EmailButton
|
<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 })}
|
{t(`${namespace}:linkText`, { orderNr })}
|
||||||
</EmailButton>
|
</EmailButton>
|
||||||
<Text>
|
<Text>
|
||||||
{t(`${namespace}:ifButtonDisabled`)}{' '}
|
{t(`${namespace}:ifButtonDisabled`)}{' '}
|
||||||
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
|
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`}
|
||||||
</Text>
|
</Text>
|
||||||
<CommonFooter t={t} />
|
<CommonFooter t={t} />
|
||||||
</EmailContent>
|
</EmailContent>
|
||||||
@@ -92,6 +88,5 @@ export async function renderDoctorSummaryReceivedEmail({
|
|||||||
return {
|
return {
|
||||||
html,
|
html,
|
||||||
subject,
|
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">
|
<Section className="mb-[16px] mt-[16px] text-center">
|
||||||
<Button className={'w-full rounded bg-neutral-950 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}
|
{props.otp}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import {
|
|||||||
} from '@react-email/components';
|
} from '@react-email/components';
|
||||||
|
|
||||||
import { BodyStyle } from '../components/body-style';
|
import { BodyStyle } from '../components/body-style';
|
||||||
|
import CommonFooter from '../components/common-footer';
|
||||||
import { EmailContent } from '../components/content';
|
import { EmailContent } from '../components/content';
|
||||||
import { EmailHeader } from '../components/header';
|
import { EmailHeader } from '../components/header';
|
||||||
import { EmailHeading } from '../components/heading';
|
import { EmailHeading } from '../components/heading';
|
||||||
import { EmailWrapper } from '../components/wrapper';
|
import { EmailWrapper } from '../components/wrapper';
|
||||||
import { initializeEmailI18n } from '../lib/i18n';
|
import { initializeEmailI18n } from '../lib/i18n';
|
||||||
import CommonFooter from '../components/common-footer';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
analysisPackageName: string;
|
analysisPackageName: string;
|
||||||
@@ -31,7 +31,10 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) {
|
|||||||
namespace: [namespace, 'common'],
|
namespace: [namespace, 'common'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewText = t(`${namespace}:previewText`);
|
const previewText = t(`${namespace}:previewText`, {
|
||||||
|
analysisPackageName: props.analysisPackageName,
|
||||||
|
});
|
||||||
|
|
||||||
const subject = t(`${namespace}:subject`, {
|
const subject = t(`${namespace}:subject`, {
|
||||||
analysisPackageName: props.analysisPackageName,
|
analysisPackageName: props.analysisPackageName,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ export * from './emails/otp.email';
|
|||||||
export * from './emails/company-offer.email';
|
export * from './emails/company-offer.email';
|
||||||
export * from './emails/synlab.email';
|
export * from './emails/synlab.email';
|
||||||
export * from './emails/doctor-summary-received.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": {
|
"footer": {
|
||||||
"lines1": "MedReport",
|
"lines1": "MedReport",
|
||||||
"lines2": "E-mail: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
|
"lines2": "Электронная почта: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
|
||||||
"lines3": "Klienditugi: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
|
"lines3": "Служба поддержки: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
|
||||||
"lines4": "<a href=\"https://www.medreport.ee\">www.medreport.ee</a>"
|
"lines4": "<a href=\"https://www.medreport.ee\">www.medreport.ee</a>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"subject": "Uus ettevõtte liitumispäring",
|
"subject": "Новый запрос на присоединение компании",
|
||||||
"previewText": "Ettevõte {{companyName}} soovib pakkumist",
|
"previewText": "Компания {{companyName}} запрашивает предложение",
|
||||||
"companyName": "Ettevõtte nimi:",
|
"companyName": "Название компании:",
|
||||||
"contactPerson": "Kontaktisik:",
|
"contactPerson": "Контактное лицо:",
|
||||||
"email": "E-mail:",
|
"email": "Электронная почта:",
|
||||||
"phone": "Telefon:"
|
"phone": "Телефон:"
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"subject": "Saabus arsti kokkuvõtte tellimusele {{orderNr}}",
|
"subject": "Получено заключение врача по заказу {{orderNr}}",
|
||||||
"previewText": "Arst on saatnud kokkuvõtte sinu analüüsitulemustele.",
|
"previewText": "Врач отправил заключение по вашим результатам анализа.",
|
||||||
"hello": "Tere, {{displayName}}",
|
"hello": "Здравствуйте, {{displayName}}",
|
||||||
"summaryReceivedForOrder": "Arst on koostanud selgitava kokkuvõtte sinu tellitud analüüsidele.",
|
"summaryReceivedForOrder": "Врач подготовил пояснительное заключение по заказанным вами анализам.",
|
||||||
"linkText": "Vaata kokkuvõtet",
|
"linkText": "Посмотреть заключение",
|
||||||
"ifButtonDisabled": "Kui nupule vajutamine ei toimi, kopeeri see link oma brauserisse:"
|
"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}}",
|
"subject": "Ваш заказ Medreport подтвержден - {{analysisPackageName}}",
|
||||||
"previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
"previewText": "Ваш заказ Medreport подтвержден - {{analysisPackageName}}",
|
||||||
"heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
"heading": "Ваш заказ Medreport подтвержден - {{analysisPackageName}}",
|
||||||
"hello": "Tere {{personName}},",
|
"hello": "Здравствуйте, {{personName}},",
|
||||||
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Synlab - {{partnerLocationName}}",
|
"lines1": "Направление на исследование {{analysisPackageName}} было отправлено в лабораторию в цифровом виде. Пожалуйста, сдайте анализы: 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>",
|
"lines2": "<i>Если вы не можете посетить выбранный пункт сдачи анализов, вы можете обратиться в удобный для вас пункт - <a href=\"https://medreport.ee/et/verevotupunktid\">посмотреть адреса и часы работы</a>.</i>",
|
||||||
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
"lines3": "Рекомендуется сдавать анализы утром (до 12:00) натощак (можно пить воду).",
|
||||||
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
|
"lines4": "В пункте сдачи анализов выберите в системе очереди: <strong>направления</strong> -> <strong>направление от специалиста</strong>.",
|
||||||
"lines5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.",
|
"lines5": "Если у вас возникнут дополнительные вопросы, смело свяжитесь с нами.",
|
||||||
"lines6": "SYNLAB klienditoe telefon: <a href=\"tel:+37217123\">17123</a>"
|
"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'];
|
type UpdateData = Database['medreport']['Tables']['accounts']['Update'];
|
||||||
|
|
||||||
export function useUpdateAccountData(accountId: string) {
|
export function useUpdateAccountData(accountId?: string) {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
|
|
||||||
const mutationKey = ['account:data', accountId];
|
const mutationKey = ['account:data', accountId];
|
||||||
|
|
||||||
const mutationFn = async (data: UpdateData) => {
|
const mutationFn = async (data: UpdateData) => {
|
||||||
|
if (!accountId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await client
|
const response = await client
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
|||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
import { UserAnalysis } from '../types/accounts';
|
import {
|
||||||
|
AnalysisResultDetails,
|
||||||
|
UserAnalysis,
|
||||||
|
UserAnalysisResponse,
|
||||||
|
} from '../types/accounts';
|
||||||
|
|
||||||
export type AccountWithParams =
|
export type AccountWithParams =
|
||||||
Database['medreport']['Tables']['accounts']['Row'] & {
|
Database['medreport']['Tables']['accounts']['Row'] & {
|
||||||
@@ -184,7 +188,49 @@ class AccountsApi {
|
|||||||
return response.data?.customer_id;
|
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 authUser = await this.client.auth.getUser();
|
||||||
const { data, error: userError } = authUser;
|
const { data, error: userError } = authUser;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
export type UserAnalysisElement =
|
export type UserAnalysisElement =
|
||||||
@@ -15,3 +17,51 @@ export enum ApplicationRoleEnum {
|
|||||||
Doctor = 'doctor',
|
Doctor = 'doctor',
|
||||||
SuperAdmin = 'super_admin',
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
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' +
|
' 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'
|
' 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(),
|
email: z.string().nullable(),
|
||||||
height: z.number().optional().nullable(),
|
height: z.number().optional().nullable(),
|
||||||
weight: z.number().optional().nullable(),
|
weight: z.number().optional().nullable(),
|
||||||
|
preferred_locale: z.string().nullable(),
|
||||||
});
|
});
|
||||||
export type Patient = z.infer<typeof PatientSchema>;
|
export type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const AccountSchema = z.object({
|
|||||||
last_name: z.string().nullable(),
|
last_name: z.string().nullable(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
primary_owner_user_id: z.string(),
|
primary_owner_user_id: z.string(),
|
||||||
|
preferred_locale: z.string().nullable(),
|
||||||
});
|
});
|
||||||
export type Account = z.infer<typeof AccountSchema>;
|
export type Account = z.infer<typeof AccountSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import 'server-only';
|
|||||||
|
|
||||||
import { isBefore } from 'date-fns';
|
import { isBefore } from 'date-fns';
|
||||||
|
|
||||||
|
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
||||||
import { getFullName } from '@kit/shared/utils';
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
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 { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
|
||||||
import {
|
import {
|
||||||
AnalysisResponseBase,
|
AnalysisResponseBase,
|
||||||
@@ -54,7 +55,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
supabase
|
supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.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),
|
.in('primary_owner_user_id', userIds),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
? await supabase
|
? await supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.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)
|
.in('primary_owner_user_id', doctorUserIds)
|
||||||
: { data: [] };
|
: { data: [] };
|
||||||
|
|
||||||
@@ -408,7 +409,7 @@ export async function getAnalysisResultsForDoctor(
|
|||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select(
|
.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)`,
|
account_params(height,weight)`,
|
||||||
)
|
)
|
||||||
.eq('is_personal_account', true)
|
.eq('is_personal_account', true)
|
||||||
@@ -472,6 +473,7 @@ export async function getAnalysisResultsForDoctor(
|
|||||||
personal_code,
|
personal_code,
|
||||||
phone,
|
phone,
|
||||||
account_params,
|
account_params,
|
||||||
|
preferred_locale,
|
||||||
} = accountWithParams[0];
|
} = accountWithParams[0];
|
||||||
|
|
||||||
const analysisResponseElementsWithPreviousData = [];
|
const analysisResponseElementsWithPreviousData = [];
|
||||||
@@ -503,6 +505,7 @@ export async function getAnalysisResultsForDoctor(
|
|||||||
},
|
},
|
||||||
doctorFeedback: doctorFeedback?.[0],
|
doctorFeedback: doctorFeedback?.[0],
|
||||||
patient: {
|
patient: {
|
||||||
|
preferred_locale,
|
||||||
userId: primary_owner_user_id,
|
userId: primary_owner_user_id,
|
||||||
accountId,
|
accountId,
|
||||||
firstName: name,
|
firstName: name,
|
||||||
@@ -638,7 +641,7 @@ export async function submitFeedback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'COMPLETED') {
|
if (status === 'COMPLETED') {
|
||||||
const [{ data: recipient }, { data: medusaOrderIds }] = await Promise.all([
|
const [{ data: recipient }, { data: analysisOrder }] = await Promise.all([
|
||||||
supabase
|
supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
@@ -653,24 +656,33 @@ export async function submitFeedback(
|
|||||||
.eq('id', analysisOrderId)
|
.eq('id', analysisOrderId)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.throwOnError(),
|
.throwOnError(),
|
||||||
|
supabase
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_orders')
|
||||||
|
.update({ status: 'COMPLETED' })
|
||||||
|
.eq('id', analysisOrderId)
|
||||||
|
.throwOnError(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!recipient?.[0]?.email) {
|
if (!recipient?.[0]?.email) {
|
||||||
throw new Error('Could not find user email.');
|
throw new Error('Could not find user email.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!medusaOrderIds?.[0]?.id) {
|
if (!analysisOrder?.[0]?.id) {
|
||||||
throw new Error('Could not retrieve order.');
|
throw new Error('Could not retrieve order.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { preferred_locale, name, last_name, email } = recipient[0];
|
const { preferred_locale, name, last_name, email } = recipient[0];
|
||||||
|
|
||||||
await sendDoctorSummaryCompletedEmail(
|
await sendEmailFromTemplate(
|
||||||
preferred_locale ?? 'et',
|
renderDoctorSummaryReceivedEmail,
|
||||||
getFullName(name, last_name),
|
{
|
||||||
|
language: preferred_locale ?? 'et',
|
||||||
|
recipientName: getFullName(name, last_name),
|
||||||
|
orderNr: analysisOrder?.[0]?.medusa_order_id ?? '',
|
||||||
|
analysisOrderId: analysisOrder[0].id,
|
||||||
|
},
|
||||||
email,
|
email,
|
||||||
medusaOrderIds?.[0]?.medusa_order_id ?? '',
|
|
||||||
medusaOrderIds[0].id,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import SelectAnalysisPackage, { AnalysisPackageWithVariant } from './select-analysis-package';
|
import SelectAnalysisPackage, {
|
||||||
|
AnalysisPackageWithVariant,
|
||||||
|
} from './select-analysis-package';
|
||||||
|
|
||||||
export default function SelectAnalysisPackages({
|
export default function SelectAnalysisPackages({
|
||||||
analysisPackages,
|
analysisPackages,
|
||||||
@@ -10,11 +12,16 @@ export default function SelectAnalysisPackages({
|
|||||||
countryCode: string;
|
countryCode: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-6">
|
<div className="grid gap-6 sm:grid-cols-3">
|
||||||
{analysisPackages.length > 0 ? analysisPackages.map(
|
{analysisPackages.length > 0 ? (
|
||||||
(analysisPackage) => (
|
analysisPackages.map((analysisPackage) => (
|
||||||
<SelectAnalysisPackage key={analysisPackage.title} analysisPackage={analysisPackage} countryCode={countryCode} />
|
<SelectAnalysisPackage
|
||||||
)) : (
|
key={analysisPackage.title}
|
||||||
|
analysisPackage={analysisPackage}
|
||||||
|
countryCode={countryCode}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<h4>
|
<h4>
|
||||||
<Trans i18nKey="order-analysis-package:noPackagesAvailable" />
|
<Trans i18nKey="order-analysis-package:noPackagesAvailable" />
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
@@ -35,12 +35,6 @@ const routes = [
|
|||||||
Icon: <ShoppingCart className={iconClasses} />,
|
Icon: <ShoppingCart className={iconClasses} />,
|
||||||
end: true,
|
end: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'common:routes.analysisResults',
|
|
||||||
path: pathsConfig.app.analysisResults,
|
|
||||||
Icon: <TestTube2 className={iconClasses} />,
|
|
||||||
end: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'common:routes.orderAnalysisPackage',
|
label: 'common:routes.orderAnalysisPackage',
|
||||||
path: pathsConfig.app.orderAnalysisPackage,
|
path: pathsConfig.app.orderAnalysisPackage,
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './use-csrf-token';
|
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]);
|
}, [currentLanguage]);
|
||||||
|
|
||||||
const userId = user?.id;
|
const userId = user?.id;
|
||||||
const updateAccountMutation = useUpdateAccountData(userId!);
|
const updateAccountMutation = useUpdateAccountData(userId);
|
||||||
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
||||||
|
|
||||||
const updateLanguagePreference = async (
|
const updateLanguagePreference = async (
|
||||||
@@ -52,6 +52,10 @@ export function LanguageSelector({
|
|||||||
onChange(locale);
|
onChange(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return i18n.changeLanguage(locale);
|
||||||
|
}
|
||||||
|
|
||||||
const promise = updateAccountMutation
|
const promise = updateAccountMutation
|
||||||
.mutateAsync({
|
.mutateAsync({
|
||||||
preferred_locale: locale,
|
preferred_locale: locale,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { cn } from '../../lib/utils';
|
|
||||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
import { LanguageSelector } from '@kit/ui/language-selector';
|
||||||
|
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
logo?: React.ReactNode;
|
logo?: React.ReactNode;
|
||||||
navigation?: React.ReactNode;
|
navigation?: React.ReactNode;
|
||||||
@@ -22,11 +24,16 @@ export const Header: React.FC<HeaderProps> = function ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="grid h-14 grid-cols-3 items-center">
|
<div className="2xs:h-14 2xs:grid-cols-3 grid h-24 w-full items-center">
|
||||||
<div className={'mx-auto md:mx-0'}>{logo}</div>
|
<div className={'mx-auto flex'}>{logo}</div>
|
||||||
<div className="order-first md:order-none">{navigation}</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function PageWithSidebar(props: PageProps) {
|
|||||||
>
|
>
|
||||||
{MobileNavigation}
|
{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}
|
{Children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +58,7 @@ export function PageMobileNavigation(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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,
|
props.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -128,5 +128,6 @@
|
|||||||
"updateRoleLoading": "Updating role...",
|
"updateRoleLoading": "Updating role...",
|
||||||
"updatePreferredLocaleSuccess": "Language preference updated",
|
"updatePreferredLocaleSuccess": "Language preference updated",
|
||||||
"updatePreferredLocaleError": "Language preference update failed",
|
"updatePreferredLocaleError": "Language preference update failed",
|
||||||
"updatePreferredLocaleLoading": "Updating language preference..."
|
"updatePreferredLocaleLoading": "Updating language preference...",
|
||||||
|
"doctorAnalysisSummary": "Doctor's summary"
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,6 @@
|
|||||||
"normal": "Normal range"
|
"normal": "Normal range"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Order number {{orderNumber}}"
|
"orderTitle": "Order number {{orderNumber}}",
|
||||||
|
"view": "View results"
|
||||||
}
|
}
|
||||||
@@ -116,5 +116,6 @@
|
|||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"invalidDataError": "Invalid data submitted"
|
"invalidDataError": "Invalid data submitted",
|
||||||
|
"language": "Language"
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"assignedTo": "Doctor",
|
"assignedTo": "Doctor",
|
||||||
"resultsStatus": "Analysis results",
|
"resultsStatus": "Analysis results",
|
||||||
"waitingForNr": "Waiting for {{nr}}",
|
"waitingForNr": "Waiting for {{nr}}",
|
||||||
|
"language": "Preferred language",
|
||||||
"responsesReceived": "Results complete"
|
"responsesReceived": "Results complete"
|
||||||
},
|
},
|
||||||
"otherPatients": "Other patients",
|
"otherPatients": "Other patients",
|
||||||
|
|||||||
@@ -151,5 +151,6 @@
|
|||||||
"updateRoleLoading": "Rolli uuendatakse...",
|
"updateRoleLoading": "Rolli uuendatakse...",
|
||||||
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
|
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
|
||||||
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
|
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
|
||||||
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse..."
|
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse...",
|
||||||
|
"doctorAnalysisSummary": "Arsti kokkuvõte analüüsitulemuste kohta"
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,6 @@
|
|||||||
"normal": "Normaalne vahemik"
|
"normal": "Normaalne vahemik"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orderTitle": "Tellimus {{orderNumber}}"
|
"orderTitle": "Tellimus {{orderNumber}}",
|
||||||
|
"view": "Vaata tulemusi"
|
||||||
}
|
}
|
||||||
@@ -136,5 +136,6 @@
|
|||||||
"confirm": "Kinnita",
|
"confirm": "Kinnita",
|
||||||
"previous": "Eelmine",
|
"previous": "Eelmine",
|
||||||
"next": "Järgmine",
|
"next": "Järgmine",
|
||||||
"invalidDataError": "Vigased andmed"
|
"invalidDataError": "Vigased andmed",
|
||||||
|
"language": "Keel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"assignedTo": "Arst",
|
"assignedTo": "Arst",
|
||||||
"resultsStatus": "Analüüsitulemused",
|
"resultsStatus": "Analüüsitulemused",
|
||||||
"waitingForNr": "Ootel {{nr}}",
|
"waitingForNr": "Ootel {{nr}}",
|
||||||
|
"language": "Patsiendi keel",
|
||||||
"responsesReceived": "Tulemused koos"
|
"responsesReceived": "Tulemused koos"
|
||||||
},
|
},
|
||||||
"otherPatients": "Muud patsiendid",
|
"otherPatients": "Muud patsiendid",
|
||||||
|
|||||||
@@ -1,132 +1,155 @@
|
|||||||
{
|
{
|
||||||
"accountTabLabel": "Account Settings",
|
"accountTabLabel": "Настройки аккаунта",
|
||||||
"accountTabDescription": "Manage your account settings",
|
"accountTabDescription": "Управляйте настройками вашего аккаунта",
|
||||||
"homePage": "Home",
|
"homePage": "Главная",
|
||||||
"billingTab": "Billing",
|
"billingTab": "Оплата",
|
||||||
"settingsTab": "Settings",
|
"settingsTab": "Настройки",
|
||||||
"multiFactorAuth": "Multi-Factor Authentication",
|
"multiFactorAuth": "Многофакторная аутентификация",
|
||||||
"multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account",
|
"multiFactorAuthDescription": "Настройте метод многофакторной аутентификации для дополнительной защиты вашего аккаунта",
|
||||||
"updateProfileSuccess": "Profile successfully updated",
|
"updateProfileSuccess": "Профиль успешно обновлен",
|
||||||
"updateProfileError": "Encountered an error. Please try again",
|
"updateProfileError": "Произошла ошибка. Пожалуйста, попробуйте снова",
|
||||||
"updatePasswordSuccess": "Password update request successful",
|
"updatePasswordSuccess": "Запрос на обновление пароля выполнен успешно",
|
||||||
"updatePasswordSuccessMessage": "Your password has been successfully updated!",
|
"updatePasswordSuccessMessage": "Ваш пароль был успешно обновлен!",
|
||||||
"updatePasswordError": "Encountered an error. Please try again",
|
"updatePasswordError": "Произошла ошибка. Пожалуйста, попробуйте снова",
|
||||||
"updatePasswordLoading": "Updating password...",
|
"updatePasswordLoading": "Обновление пароля...",
|
||||||
"updateProfileLoading": "Updating profile...",
|
"updateProfileLoading": "Обновление профиля...",
|
||||||
"name": "Your Name",
|
"name": "Ваше имя",
|
||||||
"nameDescription": "Update your name to be displayed on your profile",
|
"nameDescription": "Обновите ваше имя, которое будет отображаться в профиле",
|
||||||
"emailLabel": "Email Address",
|
"emailLabel": "Адрес электронной почты",
|
||||||
"accountImage": "Your Profile Picture",
|
"accountImage": "Ваша фотография профиля",
|
||||||
"accountImageDescription": "Please choose a photo to upload as your profile picture.",
|
"accountImageDescription": "Выберите фото для загрузки в качестве изображения профиля.",
|
||||||
"profilePictureHeading": "Upload a Profile Picture",
|
"profilePictureHeading": "Загрузить фотографию профиля",
|
||||||
"profilePictureSubheading": "Choose a photo to upload as your profile picture.",
|
"profilePictureSubheading": "Выберите фото для загрузки в качестве изображения профиля.",
|
||||||
"updateProfileSubmitLabel": "Update Profile",
|
"updateProfileSubmitLabel": "Обновить профиль",
|
||||||
"updatePasswordCardTitle": "Update your Password",
|
"updatePasswordCardTitle": "Обновите ваш пароль",
|
||||||
"updatePasswordCardDescription": "Update your password to keep your account secure.",
|
"updatePasswordCardDescription": "Обновите пароль, чтобы сохранить ваш аккаунт в безопасности.",
|
||||||
"currentPassword": "Current Password",
|
"currentPassword": "Текущий пароль",
|
||||||
"newPassword": "New Password",
|
"newPassword": "Новый пароль",
|
||||||
"repeatPassword": "Repeat New Password",
|
"repeatPassword": "Повторите новый пароль",
|
||||||
"repeatPasswordDescription": "Please repeat your new password to confirm it",
|
"repeatPasswordDescription": "Пожалуйста, повторите новый пароль для подтверждения",
|
||||||
"yourPassword": "Your Password",
|
"yourPassword": "Ваш пароль",
|
||||||
"updatePasswordSubmitLabel": "Update Password",
|
"updatePasswordSubmitLabel": "Обновить пароль",
|
||||||
"updateEmailCardTitle": "Update your Email",
|
"updateEmailCardTitle": "Обновите вашу почту",
|
||||||
"updateEmailCardDescription": "Update your email address you use to login to your account",
|
"updateEmailCardDescription": "Обновите адрес электронной почты, который вы используете для входа в аккаунт",
|
||||||
"newEmail": "Your New Email",
|
"newEmail": "Новый адрес электронной почты",
|
||||||
"repeatEmail": "Repeat Email",
|
"repeatEmail": "Повторите адрес электронной почты",
|
||||||
"updateEmailSubmitLabel": "Update Email Address",
|
"updateEmailSubmitLabel": "Обновить адрес электронной почты",
|
||||||
"updateEmailSuccess": "Email update request successful",
|
"updateEmailSuccess": "Запрос на обновление почты выполнен успешно",
|
||||||
"updateEmailSuccessMessage": "We sent you an email to confirm your new email address. Please check your inbox and click on the link to confirm your new email address.",
|
"updateEmailSuccessMessage": "Мы отправили вам письмо для подтверждения нового адреса. Пожалуйста, проверьте почту и перейдите по ссылке для подтверждения.",
|
||||||
"updateEmailLoading": "Updating your email...",
|
"updateEmailLoading": "Обновление почты...",
|
||||||
"updateEmailError": "Email not updated. Please try again",
|
"updateEmailError": "Почта не обновлена. Пожалуйста, попробуйте снова",
|
||||||
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
|
"passwordNotMatching": "Пароли не совпадают. Убедитесь, что вы используете правильный пароль",
|
||||||
"emailNotMatching": "Emails do not match. Make sure you're using the correct email",
|
"emailNotMatching": "Адреса электронной почты не совпадают. Убедитесь, что вы используете правильный адрес",
|
||||||
"passwordNotChanged": "Your password has not changed",
|
"passwordNotChanged": "Ваш пароль не был изменен",
|
||||||
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
|
"emailsNotMatching": "Адреса электронной почты не совпадают. Убедитесь, что вы используете правильный адрес",
|
||||||
"cannotUpdatePassword": "You cannot update your password because your account is not linked to any.",
|
"cannotUpdatePassword": "Вы не можете обновить пароль, так как ваш аккаунт не связан ни с одним.",
|
||||||
"setupMfaButtonLabel": "Setup a new Factor",
|
"setupMfaButtonLabel": "Настроить новый фактор",
|
||||||
"multiFactorSetupErrorHeading": "Setup Failed",
|
"multiFactorSetupErrorHeading": "Сбой настройки",
|
||||||
"multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.",
|
"multiFactorSetupErrorDescription": "Извините, произошла ошибка при настройке фактора. Пожалуйста, попробуйте снова.",
|
||||||
"multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication",
|
"multiFactorAuthHeading": "Защитите ваш аккаунт с помощью многофакторной аутентификации",
|
||||||
"multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.",
|
"multiFactorModalHeading": "Используйте приложение-аутентификатор для сканирования QR-кода ниже, затем введите сгенерированный код.",
|
||||||
"factorNameLabel": "A memorable name to identify this factor",
|
"factorNameLabel": "Запоминающееся имя для идентификации этого фактора",
|
||||||
"factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14",
|
"factorNameHint": "Используйте простое для запоминания имя, чтобы легко идентифицировать этот фактор в будущем. Пример: iPhone 14",
|
||||||
"factorNameSubmitLabel": "Set factor name",
|
"factorNameSubmitLabel": "Задать имя фактора",
|
||||||
"unenrollTooltip": "Unenroll this factor",
|
"unenrollTooltip": "Удалить фактор",
|
||||||
"unenrollingFactor": "Unenrolling factor...",
|
"unenrollingFactor": "Удаление фактора...",
|
||||||
"unenrollFactorSuccess": "Factor successfully unenrolled",
|
"unenrollFactorSuccess": "Фактор успешно удален",
|
||||||
"unenrollFactorError": "Unenrolling factor failed",
|
"unenrollFactorError": "Не удалось удалить фактор",
|
||||||
"factorsListError": "Error loading factors list",
|
"factorsListError": "Ошибка загрузки списка факторов",
|
||||||
"factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.",
|
"factorsListErrorDescription": "Извините, не удалось загрузить список факторов. Пожалуйста, попробуйте снова.",
|
||||||
"factorName": "Factor Name",
|
"factorName": "Имя фактора",
|
||||||
"factorType": "Type",
|
"factorType": "Тип",
|
||||||
"factorStatus": "Status",
|
"factorStatus": "Статус",
|
||||||
"mfaEnabledSuccessTitle": "Multi-Factor authentication is enabled",
|
"mfaEnabledSuccessTitle": "Многофакторная аутентификация включена",
|
||||||
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
|
"mfaEnabledSuccessDescription": "Поздравляем! Вы успешно подключили многофакторную аутентификацию. Теперь вы сможете входить в аккаунт с помощью комбинации пароля и кода подтверждения, отправленного на ваш номер телефона.",
|
||||||
"verificationCode": "Verification Code",
|
"verificationCode": "Код подтверждения",
|
||||||
"addEmailAddress": "Add Email address",
|
"addEmailAddress": "Добавить адрес электронной почты",
|
||||||
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above",
|
"verifyActivationCodeDescription": "Введите 6-значный код, сгенерированный вашим приложением-аутентификатором, в поле выше",
|
||||||
"loadingFactors": "Loading factors...",
|
"loadingFactors": "Загрузка факторов...",
|
||||||
"enableMfaFactor": "Enable Factor",
|
"enableMfaFactor": "Включить фактор",
|
||||||
"disableMfaFactor": "Disable Factor",
|
"disableMfaFactor": "Отключить фактор",
|
||||||
"qrCodeErrorHeading": "QR Code Error",
|
"qrCodeErrorHeading": "Ошибка QR-кода",
|
||||||
"qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code",
|
"qrCodeErrorDescription": "Извините, не удалось сгенерировать QR-код",
|
||||||
"multiFactorSetupSuccess": "Factor successfully enrolled",
|
"multiFactorSetupSuccess": "Фактор успешно подключен",
|
||||||
"submitVerificationCode": "Submit Verification Code",
|
"submitVerificationCode": "Отправить код подтверждения",
|
||||||
"mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled",
|
"mfaEnabledSuccessAlert": "Многофакторная аутентификация включена",
|
||||||
"verifyingCode": "Verifying code...",
|
"verifyingCode": "Проверка кода...",
|
||||||
"invalidVerificationCodeHeading": "Invalid Verification Code",
|
"invalidVerificationCodeHeading": "Неверный код подтверждения",
|
||||||
"invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.",
|
"invalidVerificationCodeDescription": "Введенный вами код неверен. Пожалуйста, попробуйте снова.",
|
||||||
"unenrollFactorModalHeading": "Unenroll Factor",
|
"unenrollFactorModalHeading": "Удаление фактора",
|
||||||
"unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
|
"unenrollFactorModalDescription": "Вы собираетесь удалить этот фактор. Вы больше не сможете использовать его для входа в аккаунт.",
|
||||||
"unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
|
"unenrollFactorModalBody": "Вы собираетесь удалить этот фактор. Вы больше не сможете использовать его для входа в аккаунт.",
|
||||||
"unenrollFactorModalButtonLabel": "Yes, unenroll factor",
|
"unenrollFactorModalButtonLabel": "Да, удалить фактор",
|
||||||
"selectFactor": "Choose a factor to verify your identity",
|
"selectFactor": "Выберите фактор для подтверждения личности",
|
||||||
"disableMfa": "Disable Multi-Factor Authentication",
|
"disableMfa": "Отключить многофакторную аутентификацию",
|
||||||
"disableMfaButtonLabel": "Disable MFA",
|
"disableMfaButtonLabel": "Отключить MFA",
|
||||||
"confirmDisableMfaButtonLabel": "Yes, disable MFA",
|
"confirmDisableMfaButtonLabel": "Да, отключить MFA",
|
||||||
"disablingMfa": "Disabling Multi-Factor Authentication. Please wait...",
|
"disablingMfa": "Отключение многофакторной аутентификации. Пожалуйста, подождите...",
|
||||||
"disableMfaSuccess": "Multi-Factor Authentication successfully disabled",
|
"disableMfaSuccess": "Многофакторная аутентификация успешно отключена",
|
||||||
"disableMfaError": "Sorry, we encountered an error. MFA has not been disabled.",
|
"disableMfaError": "Извините, произошла ошибка. MFA не была отключена.",
|
||||||
"sendingEmailVerificationLink": "Sending Email...",
|
"sendingEmailVerificationLink": "Отправка письма...",
|
||||||
"sendEmailVerificationLinkSuccess": "Verification link successfully sent",
|
"sendEmailVerificationLinkSuccess": "Ссылка для подтверждения успешно отправлена",
|
||||||
"sendEmailVerificationLinkError": "Sorry, we weren't able to send you the email",
|
"sendEmailVerificationLinkError": "Извините, не удалось отправить письмо",
|
||||||
"sendVerificationLinkSubmitLabel": "Send Verification Link",
|
"sendVerificationLinkSubmitLabel": "Отправить ссылку для подтверждения",
|
||||||
"sendVerificationLinkSuccessLabel": "Email sent! Check your Inbox",
|
"sendVerificationLinkSuccessLabel": "Письмо отправлено! Проверьте почту",
|
||||||
"verifyEmailAlertHeading": "Please verify your email to enable MFA",
|
"verifyEmailAlertHeading": "Пожалуйста, подтвердите вашу почту, чтобы включить MFA",
|
||||||
"verificationLinkAlertDescription": "Your email is not yet verified. Please verify your email to be able to set up Multi-Factor Authentication.",
|
"verificationLinkAlertDescription": "Ваша почта еще не подтверждена. Пожалуйста, подтвердите ее, чтобы настроить многофакторную аутентификацию.",
|
||||||
"authFactorName": "Factor Name (optional)",
|
"authFactorName": "Имя фактора (необязательно)",
|
||||||
"authFactorNameHint": "Assign a name that helps you remember the phone number used",
|
"authFactorNameHint": "Присвойте имя, которое поможет вам запомнить номер телефона, используемый для входа",
|
||||||
"loadingUser": "Loading user details. Please wait...",
|
"loadingUser": "Загрузка данных пользователя. Пожалуйста, подождите...",
|
||||||
"linkPhoneNumber": "Link Phone Number",
|
"linkPhoneNumber": "Привязать номер телефона",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Опасная зона",
|
||||||
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.",
|
"dangerZoneDescription": "Некоторые действия нельзя отменить. Будьте осторожны.",
|
||||||
"deleteAccount": "Delete your Account",
|
"deleteAccount": "Удалить аккаунт",
|
||||||
"deletingAccount": "Deleting account. Please wait...",
|
"deletingAccount": "Удаление аккаунта. Пожалуйста, подождите...",
|
||||||
"deleteAccountDescription": "This will delete your account and the accounts you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone.",
|
"deleteAccountDescription": "Это удалит ваш аккаунт и связанные с ним учетные записи. Также мы немедленно отменим все активные подписки. Это действие нельзя отменить.",
|
||||||
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
|
"deleteProfileConfirmationInputLabel": "Введите DELETE для подтверждения",
|
||||||
"deleteAccountErrorHeading": "Sorry, we couldn't delete your account",
|
"deleteAccountErrorHeading": "Извините, не удалось удалить аккаунт",
|
||||||
"needsReauthentication": "Reauthentication Required",
|
"needsReauthentication": "Требуется повторная аутентификация",
|
||||||
"needsReauthenticationDescription": "You need to reauthenticate to change your password. Please sign out and sign in again to change your password.",
|
"needsReauthenticationDescription": "Необходимо повторно войти в систему, чтобы изменить пароль. Пожалуйста, выйдите и войдите снова.",
|
||||||
"language": "Language",
|
"language": "Язык",
|
||||||
"languageDescription": "Choose your preferred language",
|
"languageDescription": "Выберите предпочитаемый язык",
|
||||||
"noTeamsYet": "You don't have any teams yet.",
|
"noTeamsYet": "У вас пока нет команд.",
|
||||||
"createTeam": "Create a team to get started.",
|
"createTeam": "Создайте команду, чтобы начать.",
|
||||||
"createTeamButtonLabel": "Create a Team",
|
"createTeamButtonLabel": "Создать команду",
|
||||||
"createCompanyAccount": "Create Company Account",
|
"createCompanyAccount": "Создать аккаунт компании",
|
||||||
"updateConsentSuccess": "Consent successfully updated",
|
"requestCompanyAccount": {
|
||||||
"updateConsentError": "Encountered an error. Please try again",
|
"title": "Данные компании",
|
||||||
"updateConsentLoading": "Updating consent...",
|
"description": "Чтобы получить предложение, пожалуйста, введите данные компании, с которой вы планируете использовать MedReport.",
|
||||||
|
"button": "Запросить предложение",
|
||||||
|
"successTitle": "Запрос успешно отправлен!",
|
||||||
|
"successDescription": "Мы ответим вам при первой возможности",
|
||||||
|
"successButton": "Вернуться на главную"
|
||||||
|
},
|
||||||
|
"updateAccount": {
|
||||||
|
"title": "Личные данные",
|
||||||
|
"description": "Пожалуйста, введите свои личные данные для продолжения",
|
||||||
|
"button": "Продолжить",
|
||||||
|
"userConsentLabel": "Я согласен на использование моих персональных данных на платформе",
|
||||||
|
"userConsentUrlTitle": "Посмотреть политику обработки персональных данных"
|
||||||
|
},
|
||||||
|
"consentModal": {
|
||||||
|
"title": "Прежде чем начать",
|
||||||
|
"description": "Вы даете согласие на использование ваших медицинских данных в анонимной форме для статистики работодателя? Данные будут обезличены и помогут компании лучше поддерживать здоровье сотрудников.",
|
||||||
|
"reject": "Не даю согласие",
|
||||||
|
"accept": "Да, даю согласие"
|
||||||
|
},
|
||||||
|
"updateConsentSuccess": "Согласия обновлены",
|
||||||
|
"updateConsentError": "Что-то пошло не так. Пожалуйста, попробуйте снова",
|
||||||
|
"updateConsentLoading": "Обновление согласий...",
|
||||||
"consentToAnonymizedCompanyData": {
|
"consentToAnonymizedCompanyData": {
|
||||||
"label": "Consent to be included in employer statistics",
|
"label": "Согласен участвовать в статистике работодателя",
|
||||||
"description": "Consent to be included in anonymized company statistics"
|
"description": "Я согласен на использование моих медицинских данных в анонимной форме для статистики работодателя"
|
||||||
},
|
},
|
||||||
"analysisResults": {
|
"membershipConfirmation": {
|
||||||
"pageTitle": "My analysis results"
|
"successTitle": "Здравствуйте, {{firstName}} {{lastName}}",
|
||||||
|
"successDescription": "Ваш медицинский аккаунт активирован и готов к использованию!",
|
||||||
|
"successButton": "Продолжить"
|
||||||
},
|
},
|
||||||
"updateRoleSuccess": "Role updated",
|
"updateRoleSuccess": "Роль обновлена",
|
||||||
"updateRoleError": "Something went wrong, please try again",
|
"updateRoleError": "Что-то пошло не так. Пожалуйста, попробуйте снова",
|
||||||
"updateRoleLoading": "Updating role...",
|
"updateRoleLoading": "Обновление роли...",
|
||||||
"updatePreferredLocaleSuccess": "Language preference updated",
|
"updatePreferredLocaleSuccess": "Предпочитаемый язык обновлен",
|
||||||
"updatePreferredLocaleError": "Language preference update failed",
|
"updatePreferredLocaleError": "Не удалось обновить предпочитаемый язык",
|
||||||
"updatePreferredLocaleLoading": "Updating language preference..."
|
"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",
|
"signUpHeading": "Создать аккаунт",
|
||||||
"signUp": "Sign Up",
|
"signUp": "Зарегистрироваться",
|
||||||
"signUpSubheading": "Fill the form below to create an account.",
|
"signUpSubheading": "Заполните форму ниже, чтобы создать аккаунт.",
|
||||||
"signInHeading": "Sign in to your account",
|
"signInHeading": "Войдите в свой аккаунт",
|
||||||
"signInSubheading": "Welcome back! Please enter your details",
|
"signInSubheading": "С возвращением! Пожалуйста, введите свои данные",
|
||||||
"signIn": "Sign In",
|
"signIn": "Войти",
|
||||||
"getStarted": "Get started",
|
"getStarted": "Начать",
|
||||||
"updatePassword": "Update Password",
|
"updatePassword": "Обновить пароль",
|
||||||
"signOut": "Sign out",
|
"signOut": "Выйти",
|
||||||
"signingIn": "Signing in...",
|
"signingIn": "Вход...",
|
||||||
"signingUp": "Signing up...",
|
"signingUp": "Регистрация...",
|
||||||
"doNotHaveAccountYet": "Do not have an account yet?",
|
"doNotHaveAccountYet": "Еще нет аккаунта?",
|
||||||
"alreadyHaveAnAccount": "Already have an account?",
|
"alreadyHaveAnAccount": "Уже есть аккаунт?",
|
||||||
"signUpToAcceptInvite": "Please sign in/up to accept the invite",
|
"signUpToAcceptInvite": "Пожалуйста, войдите или зарегистрируйтесь, чтобы принять приглашение",
|
||||||
"clickToAcceptAs": "Click the button below to accept the invite with as <b>{{email}}</b>",
|
"clickToAcceptAs": "Нажмите кнопку ниже, чтобы принять приглашение как <b>{{email}}</b>",
|
||||||
"acceptInvite": "Accept invite",
|
"acceptInvite": "Принять приглашение",
|
||||||
"acceptingInvite": "Accepting Invite...",
|
"acceptingInvite": "Принятие приглашения...",
|
||||||
"acceptInviteSuccess": "Invite successfully accepted",
|
"acceptInviteSuccess": "Приглашение успешно принято",
|
||||||
"acceptInviteError": "Error encountered while accepting invite",
|
"acceptInviteError": "Произошла ошибка при принятии приглашения",
|
||||||
"acceptInviteWithDifferentAccount": "Want to accept the invite with a different account?",
|
"acceptInviteWithDifferentAccount": "Хотите принять приглашение с другим аккаунтом?",
|
||||||
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead",
|
"alreadyHaveAccountStatement": "У меня уже есть аккаунт, я хочу войти",
|
||||||
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead",
|
"doNotHaveAccountStatement": "У меня нет аккаунта, я хочу зарегистрироваться",
|
||||||
"signInWithProvider": "Sign in with {{provider}}",
|
"signInWithProvider": "Войти через {{provider}}",
|
||||||
"signInWithPhoneNumber": "Sign in with Phone Number",
|
"signInWithPhoneNumber": "Войти по номеру телефона",
|
||||||
"signInWithEmail": "Sign in with Email",
|
"signInWithEmail": "Войти по Email",
|
||||||
"signUpWithEmail": "Sign up with Email",
|
"signUpWithEmail": "Зарегистрироваться по Email",
|
||||||
"passwordHint": "Ensure it's at least 8 characters",
|
"passwordHint": "Убедитесь, что пароль содержит не менее 8 символов",
|
||||||
"repeatPasswordHint": "Type your password again",
|
"repeatPasswordHint": "Введите пароль еще раз",
|
||||||
"repeatPassword": "Repeat password",
|
"repeatPassword": "Повторите пароль",
|
||||||
"passwordForgottenQuestion": "Password forgotten?",
|
"passwordForgottenQuestion": "Забыли пароль?",
|
||||||
"passwordResetLabel": "Reset Password",
|
"passwordResetLabel": "Сбросить пароль",
|
||||||
"passwordResetSubheading": "Enter your email address below. You will receive a link to reset your password.",
|
"passwordResetSubheading": "Введите ваш email ниже. Вы получите ссылку для сброса пароля.",
|
||||||
"passwordResetSuccessMessage": "Check your Inbox! We emailed you a link for resetting your Password.",
|
"passwordResetSuccessMessage": "Проверьте почту! Мы отправили вам ссылку для сброса пароля.",
|
||||||
"passwordRecoveredQuestion": "Password recovered?",
|
"passwordRecoveredQuestion": "Восстановили пароль?",
|
||||||
"passwordLengthError": "Please provide a password with at least 6 characters",
|
"passwordLengthError": "Пожалуйста, введите пароль длиной не менее 6 символов",
|
||||||
"sendEmailLink": "Send Email Link",
|
"sendEmailLink": "Отправить ссылку на почту",
|
||||||
"sendingEmailLink": "Sending Email Link...",
|
"sendingEmailLink": "Отправка ссылки...",
|
||||||
"sendLinkSuccessDescription": "Check your email, we just sent you a link. Follow the link to sign in.",
|
"sendLinkSuccessDescription": "Проверьте вашу почту, мы только что отправили вам ссылку. Перейдите по ней, чтобы войти.",
|
||||||
"sendLinkSuccess": "We sent you a link by email",
|
"sendLinkSuccess": "Мы отправили вам ссылку по электронной почте",
|
||||||
"sendLinkSuccessToast": "Link successfully sent",
|
"sendLinkSuccessToast": "Ссылка успешно отправлена",
|
||||||
"getNewLink": "Get a new link",
|
"getNewLink": "Получить новую ссылку",
|
||||||
"verifyCodeHeading": "Verify your account",
|
"verifyCodeHeading": "Подтвердите ваш аккаунт",
|
||||||
"verificationCode": "Verification Code",
|
"verificationCode": "Код подтверждения",
|
||||||
"verificationCodeHint": "Enter the code we sent you by SMS",
|
"verificationCodeHint": "Введите код, который мы отправили вам по SMS",
|
||||||
"verificationCodeSubmitButtonLabel": "Submit Verification Code",
|
"verificationCodeSubmitButtonLabel": "Отправить код",
|
||||||
"sendingMfaCode": "Sending Verification Code...",
|
"sendingMfaCode": "Отправка кода подтверждения...",
|
||||||
"verifyingMfaCode": "Verifying code...",
|
"verifyingMfaCode": "Проверка кода...",
|
||||||
"sendMfaCodeError": "Sorry, we couldn't send you a verification code",
|
"sendMfaCodeError": "Извините, не удалось отправить код подтверждения",
|
||||||
"verifyMfaCodeSuccess": "Code verified! Signing you in...",
|
"verifyMfaCodeSuccess": "Код подтвержден! Входим в систему...",
|
||||||
"verifyMfaCodeError": "Ops! It looks like the code is not correct",
|
"verifyMfaCodeError": "Упс! Похоже, код неверный",
|
||||||
"reauthenticate": "Reauthenticate",
|
"reauthenticate": "Повторная аутентификация",
|
||||||
"reauthenticateDescription": "For security reasons, we need you to re-authenticate",
|
"reauthenticateDescription": "В целях безопасности вам нужно пройти повторную аутентификацию",
|
||||||
"errorAlertHeading": "Sorry, we could not authenticate you",
|
"errorAlertHeading": "Извините, не удалось вас аутентифицировать",
|
||||||
"emailConfirmationAlertHeading": "We sent you a confirmation email.",
|
"emailConfirmationAlertHeading": "Мы отправили вам письмо для подтверждения.",
|
||||||
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.",
|
"emailConfirmationAlertBody": "Добро пожаловать! Пожалуйста, проверьте вашу почту и перейдите по ссылке, чтобы подтвердить аккаунт.",
|
||||||
"resendLink": "Resend link",
|
"resendLink": "Отправить ссылку снова",
|
||||||
"resendLinkSuccessDescription": "We sent you a new link to your email! Follow the link to sign in.",
|
"resendLinkSuccessDescription": "Мы отправили новую ссылку на вашу почту! Перейдите по ней, чтобы войти.",
|
||||||
"resendLinkSuccess": "Check your email!",
|
"resendLinkSuccess": "Проверьте вашу почту!",
|
||||||
"authenticationErrorAlertHeading": "Authentication Error",
|
"authenticationErrorAlertHeading": "Ошибка аутентификации",
|
||||||
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.",
|
"authenticationErrorAlertBody": "Извините, не удалось вас аутентифицировать. Пожалуйста, попробуйте снова.",
|
||||||
"sendEmailCode": "Get code to your Email",
|
"sendEmailCode": "Получить код на Email",
|
||||||
"sendingEmailCode": "Sending code...",
|
"sendingEmailCode": "Отправка кода...",
|
||||||
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
"resetPasswordError": "Извините, не удалось сбросить пароль. Пожалуйста, попробуйте снова.",
|
||||||
"emailPlaceholder": "your@email.com",
|
"emailPlaceholder": "your@email.com",
|
||||||
"inviteAlertHeading": "You have been invited to join a company",
|
"inviteAlertHeading": "Вас пригласили присоединиться к компании",
|
||||||
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
|
"inviteAlertBody": "Пожалуйста, войдите или зарегистрируйтесь, чтобы принять приглашение и присоединиться к компании.",
|
||||||
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "Я принимаю <TermsOfServiceLink /> и <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Terms of Service",
|
"termsOfService": "Условия использования",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Политика конфиденциальности",
|
||||||
"orContinueWith": "Or continue with",
|
"orContinueWith": "Или продолжить через",
|
||||||
"redirecting": "You're in! Please wait...",
|
"redirecting": "Вы вошли! Пожалуйста, подождите...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"Invalid login credentials": "The credentials entered are invalid",
|
"Invalid login credentials": "Введенные данные недействительны",
|
||||||
"User already registered": "This credential is already in use. Please try with another one.",
|
"User already registered": "Эти данные уже используются. Пожалуйста, попробуйте другие.",
|
||||||
"Email not confirmed": "Please confirm your email address before signing in",
|
"Email not confirmed": "Пожалуйста, подтвердите ваш email перед входом",
|
||||||
"default": "We have encountered an error. Please ensure you have a working internet connection and try again",
|
"default": "Произошла ошибка. Убедитесь, что у вас есть подключение к интернету, и попробуйте снова",
|
||||||
"generic": "Sorry, we weren't able to authenticate you. Please try again.",
|
"generic": "Извините, не удалось вас аутентифицировать. Пожалуйста, попробуйте снова.",
|
||||||
"link": "Sorry, we encountered an error while sending your link. Please try again.",
|
"link": "Извините, произошла ошибка при отправке ссылки. Пожалуйста, попробуйте снова.",
|
||||||
"codeVerifierMismatch": "It looks like you're trying to sign in using a different browser than the one you used to request the sign in link. Please try again using the same browser.",
|
"codeVerifierMismatch": "Похоже, вы пытаетесь войти через другой браузер, чем тот, в котором запрашивали ссылку для входа. Пожалуйста, используйте тот же браузер.",
|
||||||
"minPasswordLength": "Password must be at least 8 characters long",
|
"minPasswordLength": "Пароль должен содержать не менее 8 символов",
|
||||||
"passwordsDoNotMatch": "The passwords do not match",
|
"passwordsDoNotMatch": "Пароли не совпадают",
|
||||||
"minPasswordNumbers": "Password must contain at least one number",
|
"minPasswordNumbers": "Пароль должен содержать хотя бы одну цифру",
|
||||||
"minPasswordSpecialChars": "Password must contain at least one special character",
|
"minPasswordSpecialChars": "Пароль должен содержать хотя бы один специальный символ",
|
||||||
"uppercasePassword": "Password must contain at least one uppercase letter",
|
"uppercasePassword": "Пароль должен содержать хотя бы одну заглавную букву",
|
||||||
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action",
|
"insufficient_aal": "Пожалуйста, войдите с текущей многофакторной аутентификацией, чтобы выполнить это действие",
|
||||||
"otp_expired": "The email link has expired. Please try again.",
|
"otp_expired": "Срок действия ссылки на email истек. Пожалуйста, попробуйте снова.",
|
||||||
"same_password": "The password cannot be the same as the current password"
|
"same_password": "Пароль не может совпадать с текущим паролем"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,123 +1,143 @@
|
|||||||
{
|
{
|
||||||
"subscriptionTabSubheading": "Manage your Subscription and Billing",
|
"subscriptionTabSubheading": "Управление подпиской и оплатой",
|
||||||
"planCardTitle": "Your Plan",
|
"planCardTitle": "Ваш тариф",
|
||||||
"planCardDescription": "Below are the details of your current plan. You can change your plan or cancel your subscription at any time.",
|
"planCardDescription": "Ниже приведены детали вашего текущего тарифа. Вы можете изменить тариф или отменить подписку в любое время.",
|
||||||
"planRenewal": "Renews every {{interval}} at {{price}}",
|
"planRenewal": "Продлевается каждые {{interval}} за {{price}}",
|
||||||
"planDetails": "Plan Details",
|
"planDetails": "Детали тарифа",
|
||||||
"checkout": "Proceed to Checkout",
|
"checkout": "Перейти к оплате",
|
||||||
"trialEndsOn": "Your trial ends on",
|
"trialEndsOn": "Пробный период заканчивается",
|
||||||
"billingPortalCardButton": "Visit Billing Portal",
|
"billingPortalCardButton": "Перейти в биллинг-портал",
|
||||||
"billingPortalCardTitle": "Manage your Billing Details",
|
"billingPortalCardTitle": "Управление платежными данными",
|
||||||
"billingPortalCardDescription": "Visit your Billing Portal to manage your subscription and billing. You can update or cancel your plan, or download your invoices.",
|
"billingPortalCardDescription": "Перейдите в биллинг-портал, чтобы управлять подпиской и платежами. Вы можете изменить или отменить тариф, а также скачать счета.",
|
||||||
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
"cancelAtPeriodEndDescription": "Ваша подписка будет отменена {{- endDate }}.",
|
||||||
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
"renewAtPeriodEndDescription": "Ваша подписка будет продлена {{- endDate }}",
|
||||||
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
"noPermissionsAlertHeading": "У вас нет прав для изменения настроек оплаты",
|
||||||
"noPermissionsAlertBody": "Please contact your account admin to change the billing settings for your account.",
|
"noPermissionsAlertBody": "Пожалуйста, свяжитесь с администратором аккаунта, чтобы изменить настройки оплаты.",
|
||||||
"checkoutSuccessTitle": "Done! You're all set.",
|
"checkoutSuccessTitle": "Готово! Всё настроено.",
|
||||||
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
"checkoutSuccessDescription": "Спасибо за подписку! Мы успешно оформили вашу подписку. Подтверждение отправлено на {{ customerEmail }}.",
|
||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"checkoutSuccessBackButton": "Перейти в приложение",
|
||||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
"cannotManageBillingAlertTitle": "Вы не можете управлять оплатой",
|
||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account admin.",
|
"cannotManageBillingAlertDescription": "У вас нет прав для управления оплатой. Пожалуйста, свяжитесь с администратором аккаунта.",
|
||||||
"manageTeamPlan": "Manage your Company Plan",
|
"manageTeamPlan": "Управление корпоративным тарифом",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Выберите тариф, который соответствует потребностям вашей компании. Вы можете изменить его в любое время.",
|
||||||
"basePlan": "Base Plan",
|
"basePlan": "Базовый тариф",
|
||||||
"billingInterval": {
|
"billingInterval": {
|
||||||
"label": "Choose your billing interval",
|
"label": "Выберите интервал оплаты",
|
||||||
"month": "Billed monthly",
|
"month": "Оплата ежемесячно",
|
||||||
"year": "Billed yearly"
|
"year": "Оплата ежегодно"
|
||||||
},
|
},
|
||||||
"perMonth": "month",
|
"perMonth": "месяц",
|
||||||
"custom": "Custom Plan",
|
"custom": "Индивидуальный тариф",
|
||||||
"lifetime": "Lifetime",
|
"lifetime": "Пожизненный",
|
||||||
"trialPeriod": "{{period}} day trial",
|
"trialPeriod": "Пробный период {{period}} дней",
|
||||||
"perPeriod": "per {{period}}",
|
"perPeriod": "за {{period}}",
|
||||||
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
"redirectingToPayment": "Перенаправление на оплату. Пожалуйста, подождите...",
|
||||||
"proceedToPayment": "Proceed to Payment",
|
"proceedToPayment": "Перейти к оплате",
|
||||||
"startTrial": "Start Trial",
|
"startTrial": "Начать пробный период",
|
||||||
"perTeamMember": "Per company member",
|
"perTeamMember": "На одного сотрудника",
|
||||||
"perUnit": "Per {{unit}} usage",
|
"perUnit": "За использование {{unit}}",
|
||||||
"teamMembers": "Company Members",
|
"teamMembers": "Сотрудники компании",
|
||||||
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
"includedUpTo": "До {{upTo}} {{unit}} включено в тариф",
|
||||||
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
"fromPreviousTierUpTo": "за каждый {{unit}} для следующих {{ upTo }} {{ unit }}",
|
||||||
"andAbove": "above {{ previousTier }} {{ unit }}",
|
"andAbove": "свыше {{ previousTier }} {{ unit }}",
|
||||||
"startingAtPriceUnit": "Starting at {{price}}/{{unit}}",
|
"startingAtPriceUnit": "Начиная с {{price}}/{{unit}}",
|
||||||
"priceUnit": "{{price}}/{{unit}}",
|
"priceUnit": "{{price}}/{{unit}}",
|
||||||
"forEveryUnit": "for every {{ unit }}",
|
"forEveryUnit": "за каждый {{ unit }}",
|
||||||
"setupFee": "plus a {{ setupFee }} setup fee",
|
"setupFee": "плюс плата за установку {{ setupFee }}",
|
||||||
"perUnitIncluded": "({{included}} included)",
|
"perUnitIncluded": "({{included}} включено)",
|
||||||
"featuresLabel": "Features",
|
"featuresLabel": "Функции",
|
||||||
"detailsLabel": "Details",
|
"detailsLabel": "Подробности",
|
||||||
"planPickerLabel": "Pick your preferred plan",
|
"planPickerLabel": "Выберите тариф",
|
||||||
"planCardLabel": "Manage your Plan",
|
"planCardLabel": "Управление тарифом",
|
||||||
"planPickerAlertErrorTitle": "Error requesting checkout",
|
"planPickerAlertErrorTitle": "Ошибка при запросе оплаты",
|
||||||
"planPickerAlertErrorDescription": "There was an error requesting checkout. Please try again later.",
|
"planPickerAlertErrorDescription": "Произошла ошибка при запросе оплаты. Пожалуйста, попробуйте позже.",
|
||||||
"subscriptionCancelled": "Subscription Cancelled",
|
"subscriptionCancelled": "Подписка отменена",
|
||||||
"cancelSubscriptionDate": "Your subscription will be cancelled at the end of the period",
|
"cancelSubscriptionDate": "Ваша подписка будет отменена в конце расчетного периода",
|
||||||
"noPlanChosen": "Please choose a plan",
|
"noPlanChosen": "Пожалуйста, выберите тариф",
|
||||||
"noIntervalPlanChosen": "Please choose a billing interval",
|
"noIntervalPlanChosen": "Пожалуйста, выберите интервал оплаты",
|
||||||
"status": {
|
"status": {
|
||||||
"free": {
|
"free": {
|
||||||
"badge": "Free Plan",
|
"badge": "Бесплатный тариф",
|
||||||
"heading": "You are currently on the Free Plan",
|
"heading": "Вы используете бесплатный тариф",
|
||||||
"description": "You're on a free plan. You can upgrade to a paid plan at any time."
|
"description": "Вы на бесплатном тарифе. Вы можете перейти на платный тариф в любое время."
|
||||||
},
|
},
|
||||||
"active": {
|
"active": {
|
||||||
"badge": "Active",
|
"badge": "Активен",
|
||||||
"heading": "Your subscription is active",
|
"heading": "Ваша подписка активна",
|
||||||
"description": "Your subscription is active. You can manage your subscription and billing in the Customer Portal."
|
"description": "Ваша подписка активна. Вы можете управлять подпиской и оплатой в клиентском портале."
|
||||||
},
|
},
|
||||||
"trialing": {
|
"trialing": {
|
||||||
"badge": "Trial",
|
"badge": "Пробный период",
|
||||||
"heading": "You're on a trial",
|
"heading": "Вы используете пробный период",
|
||||||
"description": "You can enjoy the benefits of plan until the trial ends"
|
"description": "Вы можете пользоваться тарифом до окончания пробного периода"
|
||||||
},
|
},
|
||||||
"past_due": {
|
"past_due": {
|
||||||
"badge": "Past Due",
|
"badge": "Просрочено",
|
||||||
"heading": "Your invoice is past due",
|
"heading": "Ваш счет просрочен",
|
||||||
"description": "Your invoice is past due. Please update your payment method."
|
"description": "Ваш счет просрочен. Пожалуйста, обновите способ оплаты."
|
||||||
},
|
},
|
||||||
"canceled": {
|
"canceled": {
|
||||||
"badge": "Canceled",
|
"badge": "Отменено",
|
||||||
"heading": "Your subscription is canceled",
|
"heading": "Ваша подписка отменена",
|
||||||
"description": "Your subscription is canceled. It is scheduled to end at end of the billing period."
|
"description": "Ваша подписка отменена и завершится в конце расчетного периода."
|
||||||
},
|
},
|
||||||
"unpaid": {
|
"unpaid": {
|
||||||
"badge": "Unpaid",
|
"badge": "Неоплачено",
|
||||||
"heading": "Your invoice is unpaid",
|
"heading": "Ваш счет не оплачен",
|
||||||
"description": "Your invoice is unpaid. Please update your payment method."
|
"description": "Ваш счет не оплачен. Пожалуйста, обновите способ оплаты."
|
||||||
},
|
},
|
||||||
"incomplete": {
|
"incomplete": {
|
||||||
"badge": "Incomplete",
|
"badge": "Не завершено",
|
||||||
"heading": "We're waiting for your payment",
|
"heading": "Мы ждем вашу оплату",
|
||||||
"description": "We're waiting for your payment to go through. Please bear with us."
|
"description": "Мы ждем завершения вашей оплаты. Пожалуйста, подождите."
|
||||||
},
|
},
|
||||||
"incomplete_expired": {
|
"incomplete_expired": {
|
||||||
"badge": "Expired",
|
"badge": "Истек",
|
||||||
"heading": "Your payment has expired",
|
"heading": "Срок оплаты истек",
|
||||||
"description": "Your payment has expired. Please update your payment method."
|
"description": "Срок оплаты истек. Пожалуйста, обновите способ оплаты."
|
||||||
},
|
},
|
||||||
"paused": {
|
"paused": {
|
||||||
"badge": "Paused",
|
"badge": "Приостановлено",
|
||||||
"heading": "Your subscription is paused",
|
"heading": "Ваша подписка приостановлена",
|
||||||
"description": "Your subscription is paused. You can resume it at any time."
|
"description": "Ваша подписка приостановлена. Вы можете возобновить ее в любое время."
|
||||||
},
|
},
|
||||||
"succeeded": {
|
"succeeded": {
|
||||||
"badge": "Succeeded",
|
"badge": "Успешно",
|
||||||
"heading": "Your payment was successful",
|
"heading": "Оплата прошла успешно",
|
||||||
"description": "Your payment was successful. Thank you for subscribing!"
|
"description": "Оплата прошла успешно. Спасибо за подписку!"
|
||||||
},
|
},
|
||||||
"pending": {
|
"pending": {
|
||||||
"badge": "Pending",
|
"badge": "В ожидании",
|
||||||
"heading": "Your payment is pending",
|
"heading": "Оплата в ожидании",
|
||||||
"description": "Your payment is pending. Please bear with us."
|
"description": "Оплата находится в ожидании. Пожалуйста, подождите."
|
||||||
},
|
},
|
||||||
"failed": {
|
"failed": {
|
||||||
"badge": "Failed",
|
"badge": "Неудачно",
|
||||||
"heading": "Your payment failed",
|
"heading": "Оплата не прошла",
|
||||||
"description": "Your payment failed. Please update your payment method."
|
"description": "Оплата не прошла. Пожалуйста, обновите способ оплаты."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cart": {
|
"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",
|
"title": "Выберите услугу",
|
||||||
"description": "Select the appropriate service or package according to your health needs or goals.",
|
"description": "Выберите подходящую услугу или пакет в зависимости от вашей проблемы со здоровьем или цели.",
|
||||||
"analysisPackages": {
|
"analysisPackages": {
|
||||||
"title": "Analysis packages",
|
"title": "Пакеты анализов",
|
||||||
"description": "Get to know the personal analysis packages and order"
|
"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",
|
"homeTabLabel": "Главная",
|
||||||
"homeTabDescription": "Welcome to your home page",
|
"homeTabDescription": "Добро пожаловать на вашу домашнюю страницу",
|
||||||
"accountMembers": "Company Members",
|
"accountMembers": "Члены компании",
|
||||||
"membersTabDescription": "Here you can manage the members of your company.",
|
"membersTabDescription": "Здесь вы можете управлять членами вашей компании.",
|
||||||
"billingTabLabel": "Billing",
|
"billingTabLabel": "Оплата",
|
||||||
"billingTabDescription": "Manage your billing and subscription",
|
"billingTabDescription": "Управление оплатой и подпиской",
|
||||||
"dashboardTabLabel": "Dashboard",
|
"dashboardTabLabel": "Панель",
|
||||||
"settingsTabLabel": "Settings",
|
"settingsTabLabel": "Настройки",
|
||||||
"profileSettingsTabLabel": "Profile",
|
"profileSettingsTabLabel": "Профиль",
|
||||||
"subscriptionSettingsTabLabel": "Subscription",
|
"subscriptionSettingsTabLabel": "Подписка",
|
||||||
"dashboardTabDescription": "An overview of your account's activity and performance across all your projects.",
|
"dashboardTabDescription": "Обзор активности и эффективности вашей учетной записи по всем проектам.",
|
||||||
"settingsTabDescription": "Manage your settings and preferences.",
|
"settingsTabDescription": "Управляйте своими настройками и предпочтениями.",
|
||||||
"emailAddress": "Email Address",
|
"emailAddress": "Электронная почта",
|
||||||
"password": "Password",
|
"password": "Пароль",
|
||||||
"modalConfirmationQuestion": "Are you sure you want to continue?",
|
"modalConfirmationQuestion": "Вы уверены, что хотите продолжить?",
|
||||||
"imageInputLabel": "Click here to upload an image",
|
"imageInputLabel": "Нажмите здесь, чтобы загрузить изображение",
|
||||||
"cancel": "Cancel",
|
"cancel": "Отмена",
|
||||||
"clear": "Clear",
|
"clear": "Очистить",
|
||||||
"close": "Close",
|
"close": "Закрыть",
|
||||||
"notFound": "Not Found",
|
"notFound": "Не найдено",
|
||||||
"backToHomePage": "Back to Home Page",
|
"backToHomePage": "Вернуться на главную",
|
||||||
"goBack": "Go Back",
|
"goBack": "Назад",
|
||||||
"genericServerError": "Sorry, something went wrong.",
|
"genericServerError": "Извините, что-то пошло не так.",
|
||||||
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
|
"genericServerErrorHeading": "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема не исчезнет.",
|
||||||
"pageNotFound": "Sorry, this page does not exist.",
|
"pageNotFound": "Извините, эта страница не существует.",
|
||||||
"pageNotFoundSubHeading": "Apologies, the page you were looking for was not found",
|
"pageNotFoundSubHeading": "К сожалению, запрашиваемая страница не найдена",
|
||||||
"genericError": "Sorry, something went wrong.",
|
"genericError": "Извините, что-то пошло не так.",
|
||||||
"genericErrorSubHeading": "Apologies, an error occurred while processing your request. Please contact us if the issue persists.",
|
"genericErrorSubHeading": "К сожалению, произошла ошибка при обработке вашего запроса. Пожалуйста, свяжитесь с нами, если проблема не исчезнет.",
|
||||||
"anonymousUser": "Anonymous",
|
"anonymousUser": "Анонимный пользователь",
|
||||||
"tryAgain": "Try Again",
|
"tryAgain": "Попробовать снова",
|
||||||
"theme": "Theme",
|
"theme": "Тема",
|
||||||
"lightTheme": "Light",
|
"lightTheme": "Светлая",
|
||||||
"darkTheme": "Dark",
|
"darkTheme": "Тёмная",
|
||||||
"systemTheme": "System",
|
"systemTheme": "Системная",
|
||||||
"expandSidebar": "Expand Sidebar",
|
"expandSidebar": "Развернуть боковое меню",
|
||||||
"collapseSidebar": "Collapse Sidebar",
|
"collapseSidebar": "Свернуть боковое меню",
|
||||||
"documentation": "Documentation",
|
"documentation": "Документация",
|
||||||
"getStarted": "Get Started",
|
"getStarted": "Начать!",
|
||||||
"getStartedWithPlan": "Get Started with {{plan}}",
|
"getStartedWithPlan": "Начать с {{plan}}",
|
||||||
"retry": "Retry",
|
"retry": "Повторить",
|
||||||
"contactUs": "Contact Us",
|
"contactUs": "Свяжитесь с нами",
|
||||||
"loading": "Loading. Please wait...",
|
"loading": "Загрузка. Пожалуйста, подождите...",
|
||||||
"yourAccounts": "Your Accounts",
|
"yourAccounts": "Ваши аккаунты",
|
||||||
"continue": "Continue",
|
"continue": "Продолжить",
|
||||||
"skip": "Skip",
|
"skip": "Пропустить",
|
||||||
"signedInAs": "Signed in as",
|
"signedInAs": "Вы вошли как",
|
||||||
"pageOfPages": "Page {{page}} of {{total}}",
|
"pageOfPages": "Страница {{page}} / {{total}}",
|
||||||
"noData": "No data available",
|
"noData": "Нет данных",
|
||||||
"pageNotFoundHeading": "Ouch! :|",
|
"pageNotFoundHeading": "Упс! :|",
|
||||||
"errorPageHeading": "Ouch! :|",
|
"errorPageHeading": "Упс! :|",
|
||||||
"notifications": "Notifications",
|
"notifications": "Уведомления",
|
||||||
"noNotifications": "No notifications",
|
"noNotifications": "Нет уведомлений",
|
||||||
"justNow": "Just now",
|
"justNow": "Прямо сейчас",
|
||||||
"newVersionAvailable": "New version available",
|
"newVersionAvailable": "Доступна новая версия",
|
||||||
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
|
"newVersionAvailableDescription": "Доступна новая версия приложения. Рекомендуется обновить страницу, чтобы получить последние обновления и избежать возможных проблем.",
|
||||||
"newVersionSubmitButton": "Reload and Update",
|
"newVersionSubmitButton": "Перезагрузить и обновить",
|
||||||
"back": "Back",
|
"back": "Назад",
|
||||||
"welcome": "Welcome",
|
"welcome": "Добро пожаловать",
|
||||||
"shoppingCart": "Shopping cart",
|
"shoppingCart": "Корзина",
|
||||||
"search": "Search{{end}}",
|
"shoppingCartCount": "Корзина ({{count}})",
|
||||||
"myActions": "My actions",
|
"search": "Поиск{{end}}",
|
||||||
|
"myActions": "Мои действия",
|
||||||
"healthPackageComparison": {
|
"healthPackageComparison": {
|
||||||
"label": "Health package comparison",
|
"label": "Сравнение пакетов здоровья",
|
||||||
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
|
"description": "Ниже приведен персональный выбор пакета медицинского обследования на основе предварительной информации (пол, возраст и индекс массы тела). В таблице можно добавить к рекомендуемому пакету отдельные исследования."
|
||||||
},
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
"home": "Home",
|
"home": "Главная",
|
||||||
"overview": "Overview",
|
"overview": "Обзор",
|
||||||
"booking": "Booking",
|
"booking": "Забронировать время",
|
||||||
"myOrders": "My orders",
|
"myOrders": "Мои заказы",
|
||||||
"orderAnalysis": "Order analysis",
|
"analysisResults": "Результаты анализов",
|
||||||
"orderAnalysisPackage": "Order analysis package",
|
"orderAnalysisPackage": "Заказать пакет анализов",
|
||||||
"orderHealthAnalysis": "Order health analysis",
|
"orderAnalysis": "Заказать анализ",
|
||||||
"account": "Account",
|
"orderHealthAnalysis": "Заказать обследование",
|
||||||
"members": "Members",
|
"account": "Аккаунт",
|
||||||
"billing": "Billing",
|
"members": "Участники",
|
||||||
"dashboard": "Dashboard",
|
"billing": "Оплата",
|
||||||
"settings": "Settings",
|
"dashboard": "Обзор",
|
||||||
"profile": "Profile",
|
"settings": "Настройки",
|
||||||
"application": "Application"
|
"profile": "Профиль",
|
||||||
|
"application": "Приложение",
|
||||||
|
"pickTime": "Выберите время"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"owner": {
|
"owner": {
|
||||||
"label": "Admin"
|
"label": "Администратор"
|
||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"label": "Member"
|
"label": "Участник"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
"requestVerificationCode": "Request Verification Code",
|
"requestVerificationCode": "Запросить код подтверждения",
|
||||||
"requestVerificationCodeDescription": "We must verify your identity to continue with this action. We'll send a verification code to the email address {{email}}.",
|
"requestVerificationCodeDescription": "Мы должны подтвердить вашу личность для продолжения. Мы отправим код подтверждения на электронный адрес {{email}}.",
|
||||||
"sendingCode": "Sending Code...",
|
"sendingCode": "Отправка кода...",
|
||||||
"sendVerificationCode": "Send Verification Code",
|
"sendVerificationCode": "Отправить код подтверждения",
|
||||||
"enterVerificationCode": "Enter Verification Code",
|
"enterVerificationCode": "Введите код подтверждения",
|
||||||
"codeSentToEmail": "We've sent a verification code to the email address {{email}}.",
|
"codeSentToEmail": "Мы отправили код подтверждения на электронный адрес {{email}}.",
|
||||||
"verificationCode": "Verification Code",
|
"verificationCode": "Код подтверждения",
|
||||||
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email.",
|
"enterCodeFromEmail": "Введите 6-значный код, который мы отправили на вашу почту.",
|
||||||
"verifying": "Verifying...",
|
"verifying": "Проверка...",
|
||||||
"verifyCode": "Verify Code",
|
"verifyCode": "Подтвердить код",
|
||||||
"requestNewCode": "Request New Code",
|
"requestNewCode": "Запросить новый код",
|
||||||
"errorSendingCode": "Error sending code. Please try again."
|
"errorSendingCode": "Ошибка при отправке кода. Пожалуйста, попробуйте снова."
|
||||||
},
|
},
|
||||||
"cookieBanner": {
|
"cookieBanner": {
|
||||||
"title": "Hey, we use cookies 🍪",
|
"title": "Привет, мы используем куки 🍪",
|
||||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
"description": "Этот сайт использует файлы cookie, чтобы обеспечить вам наилучший опыт.",
|
||||||
"reject": "Reject",
|
"reject": "Отклонить",
|
||||||
"accept": "Accept"
|
"accept": "Принять"
|
||||||
},
|
},
|
||||||
"doctor": "Doctor",
|
"formField": {
|
||||||
"save": "Save",
|
"companyName": "Название компании",
|
||||||
"saveAsDraft": "Save as draft",
|
"contactPerson": "Контактное лицо",
|
||||||
"confirm": "Confirm",
|
"email": "Электронная почта",
|
||||||
"previous": "Previous",
|
"phone": "Телефон",
|
||||||
"next": "Next",
|
"firstName": "Имя",
|
||||||
"invalidDataError": "Invalid data submitted"
|
"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",
|
"recentlyCheckedDescription": "Отлично, вы проверили своё здоровье. Вот важные показатели для вас",
|
||||||
"respondToQuestion": "Respond",
|
"respondToQuestion": "Ответить на вопрос",
|
||||||
"gender": "Gender",
|
"gender": "Пол",
|
||||||
"male": "Male",
|
"male": "Мужчина",
|
||||||
"female": "Female",
|
"female": "Женщина",
|
||||||
"age": "Age",
|
"age": "Возраст",
|
||||||
"height": "Height",
|
"height": "Рост",
|
||||||
"weight": "Weight",
|
"weight": "Вес",
|
||||||
"bmi": "BMI",
|
"bmi": "ИМТ",
|
||||||
"bloodPressure": "Blood pressure",
|
"bloodPressure": "Давление",
|
||||||
"cholesterol": "Cholesterol",
|
"cholesterol": "Холестерин",
|
||||||
"ldlCholesterol": "LDL Cholesterol",
|
"ldlCholesterol": "ЛПНП холестерин",
|
||||||
"smoking": "Smoking",
|
"smoking": "Курение",
|
||||||
"recommendedForYou": "Recommended for you"
|
"recommendedForYou": "Рекомендации для вас",
|
||||||
|
"heroCard": {
|
||||||
|
"orderAnalysis": {
|
||||||
|
"title": "Заказать анализ",
|
||||||
|
"description": "Закажите подходящий для вас анализ"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,50 +1,51 @@
|
|||||||
{
|
{
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"dashboard": "Overview",
|
"dashboard": "Обзор",
|
||||||
"openReviews": "Open jobs",
|
"openReviews": "Свободные задания",
|
||||||
"myReviews": "My jobs",
|
"myReviews": "Мои задания",
|
||||||
"completedReviews": "Completed jobs"
|
"completedReviews": "Выполненные задания"
|
||||||
},
|
},
|
||||||
"openReviews": "Open jobs",
|
"openReviews": "Свободные задания",
|
||||||
"myReviews": "My jobs",
|
"myReviews": "Мои задания",
|
||||||
"completedReviews": "Completed jobs",
|
"completedReviews": "Выполненные задания",
|
||||||
"otherReviews": "Other jobs",
|
"otherReviews": "Другие задания",
|
||||||
"resultsTable": {
|
"resultsTable": {
|
||||||
"patientName": "Patient name",
|
"patientName": "Имя пациента",
|
||||||
"serviceName": "Service",
|
"serviceName": "Услуга",
|
||||||
"orderNr": "Order number",
|
"orderNr": "Номер заказа",
|
||||||
"time": "Time",
|
"time": "Время",
|
||||||
"assignedTo": "Doctor",
|
"assignedTo": "Врач",
|
||||||
"resultsStatus": "Analysis results",
|
"resultsStatus": "Результаты анализов",
|
||||||
"waitingForNr": "Waiting for {{nr}}",
|
"waitingForNr": "В ожидании {{nr}}",
|
||||||
"responsesReceived": "Results complete"
|
"language": "Предпочтительный язык",
|
||||||
|
"responsesReceived": "Результаты готовы"
|
||||||
},
|
},
|
||||||
"otherPatients": "Other patients",
|
"otherPatients": "Другие пациенты",
|
||||||
"analyses": "Analyses",
|
"analyses": "Анализы",
|
||||||
"open": "Open",
|
"open": "Открыть",
|
||||||
"name": "Name",
|
"name": "Имя",
|
||||||
"personalCode": "Personal code",
|
"personalCode": "Личный код",
|
||||||
"dobAndAge": "Date of birth and age",
|
"dobAndAge": "Дата рождения и возраст",
|
||||||
"bmi": "Body mass index",
|
"bmi": "Индекс массы тела",
|
||||||
"smoking": "Smoking",
|
"smoking": "Курение",
|
||||||
"phone": "Phone",
|
"phone": "Телефон",
|
||||||
"email": "Email",
|
"email": "Эл. почта",
|
||||||
"results": "Analysis results",
|
"results": "Результаты анализов",
|
||||||
"feedback": "Summary",
|
"feedback": "Заключение",
|
||||||
"selectJob": "Select",
|
"selectJob": "Выбрать",
|
||||||
"unselectJob": "Unselect",
|
"unselectJob": "Отказаться",
|
||||||
"previousResults": "Previous results ({{date}})",
|
"previousResults": "Предыдущие результаты ({{date}})",
|
||||||
"labComment": "Lab comment",
|
"labComment": "Комментарии лаборатории",
|
||||||
"confirmFeedbackModal": {
|
"confirmFeedbackModal": {
|
||||||
"title": "Confirm publishing summary",
|
"title": "Подтвердите публикацию заключения",
|
||||||
"description": "When confirmed, the summary will be published to the patient."
|
"description": "После подтверждения заключение будет опубликовано для пациента."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"UNKNOWN": "Something went wrong",
|
"UNKNOWN": "Что-то пошло не так",
|
||||||
"JOB_ASSIGNED": "Job already selected"
|
"JOB_ASSIGNED": "Задание уже занято"
|
||||||
},
|
},
|
||||||
"updateFeedbackSuccess": "Summary updated",
|
"updateFeedbackSuccess": "Заключение обновлено",
|
||||||
"updateFeedbackLoading": "Updating summary...",
|
"updateFeedbackLoading": "Обновление заключения...",
|
||||||
"updateFeedbackError": "Failed to update summary",
|
"updateFeedbackError": "Не удалось обновить заключение",
|
||||||
"feedbackLengthError": "Summary must be at least 10 characters"
|
"feedbackLengthError": "Заключение должно содержать не менее 10 символов"
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"blog": "Blog",
|
"blog": "Блог",
|
||||||
"blogSubtitle": "News and updates about the platform",
|
"blogSubtitle": "Новости и обновления о платформе",
|
||||||
"documentation": "Documentation",
|
"documentation": "Документация",
|
||||||
"documentationSubtitle": "Tutorials and guide to get started with the platform",
|
"documentationSubtitle": "Учебники и руководство по началу работы с платформой",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"faqSubtitle": "Frequently asked questions about the platform",
|
"faqSubtitle": "Часто задаваемые вопросы о платформе",
|
||||||
"pricing": "Pricing",
|
"pricing": "Цены",
|
||||||
"pricingSubtitle": "Pricing plans and payment options",
|
"pricingSubtitle": "Тарифные планы и варианты оплаты",
|
||||||
"backToBlog": "Back to blog",
|
"backToBlog": "Назад к блогу",
|
||||||
"noPosts": "No posts found",
|
"noPosts": "Посты не найдены",
|
||||||
"blogPaginationNext": "Next Page",
|
"blogPaginationNext": "Следующая страница",
|
||||||
"blogPaginationPrevious": "Previous Page",
|
"blogPaginationPrevious": "Предыдущая страница",
|
||||||
"readMore": "Read more",
|
"readMore": "Читать далее",
|
||||||
"contactFaq": "If you have any questions, please contact us",
|
"contactFaq": "Если у вас есть вопросы, пожалуйста, свяжитесь с нами",
|
||||||
"contact": "Contact",
|
"contact": "Контакты",
|
||||||
"about": "About",
|
"about": "О нас",
|
||||||
"product": "Product",
|
"product": "Продукт",
|
||||||
"legal": "Legal",
|
"legal": "Правовая информация",
|
||||||
"termsOfService": "Terms of Service",
|
"termsOfService": "Условия обслуживания",
|
||||||
"termsOfServiceDescription": "Our terms and conditions",
|
"termsOfServiceDescription": "Наши правила и условия",
|
||||||
"cookiePolicy": "Cookie Policy",
|
"cookiePolicy": "Политика использования файлов cookie",
|
||||||
"cookiePolicyDescription": "Our cookie policy and how we use them",
|
"cookiePolicyDescription": "Наша политика использования cookie и как мы их применяем",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Политика конфиденциальности",
|
||||||
"privacyPolicyDescription": "Our privacy policy and how we use your data",
|
"privacyPolicyDescription": "Наша политика конфиденциальности и как мы используем ваши данные",
|
||||||
"contactDescription": "Contact us for any questions or feedback",
|
"contactDescription": "Свяжитесь с нами по любым вопросам или отзывам",
|
||||||
"contactHeading": "Send us a message",
|
"contactHeading": "Отправьте нам сообщение",
|
||||||
"contactSubheading": "We will get back to you as soon as possible",
|
"contactSubheading": "Мы свяжемся с вами как можно скорее",
|
||||||
"contactName": "Your Name",
|
"contactName": "Ваше имя",
|
||||||
"contactEmail": "Your Email",
|
"contactEmail": "Ваш email",
|
||||||
"contactMessage": "Your Message",
|
"contactMessage": "Ваше сообщение",
|
||||||
"sendMessage": "Send Message",
|
"sendMessage": "Отправить сообщение",
|
||||||
"contactSuccess": "Your message has been sent successfully",
|
"contactSuccess": "Ваше сообщение успешно отправлено",
|
||||||
"contactError": "An error occurred while sending your message",
|
"contactError": "Произошла ошибка при отправке сообщения",
|
||||||
"contactSuccessDescription": "We have received your message and will get back to you as soon as possible",
|
"contactSuccessDescription": "Мы получили ваше сообщение и свяжемся с вами в ближайшее время",
|
||||||
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
|
"contactErrorDescription": "Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте позже",
|
||||||
"footerDescription": "Here you can add a description about your company or product",
|
"footerDescription": "Здесь вы можете добавить описание вашей компании или продукта",
|
||||||
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
|
"copyright": "© Copyright {{year}} {{product}}. Все права защищены.",
|
||||||
"heroSubtitle": "A simple, convenient, and quick overview of your health condition",
|
"heroSubtitle": "Простой, удобный и быстрый обзор вашего здоровья",
|
||||||
"notInterestedInAudit": "Currently not interested in a health audit"
|
"notInterestedInAudit": "Не хочу проходить аудит здоровья в данный момент"
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"title": "Select analysis package",
|
"title": "Выберите пакет анализов",
|
||||||
"noPackagesAvailable": "No packages available",
|
"noPackagesAvailable": "Список услуг не найден, попробуйте позже",
|
||||||
"selectThisPackage": "Select this package",
|
"selectThisPackage": "Выбрать этот пакет",
|
||||||
"selectPackage": "Select package",
|
"selectPackage": "Выберите пакет",
|
||||||
"comparePackages": "Compare packages"
|
"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": {
|
"clinicalBloodDraw": {
|
||||||
"label": "Kliiniline vereanalüüs",
|
"label": "Клинический анализ крови",
|
||||||
"description": "Pending"
|
"description": "Ожидается"
|
||||||
},
|
},
|
||||||
"crp": {
|
"crp": {
|
||||||
"label": "C-reaktiivne valk (CRP)",
|
"label": "С-реактивный белок (CRP)",
|
||||||
"description": "Pending"
|
"description": "Ожидается"
|
||||||
},
|
},
|
||||||
"ferritin": {
|
"ferritin": {
|
||||||
"label": "Ferritiin",
|
"label": "Ферритин",
|
||||||
"description": "Pending"
|
"description": "Ожидается"
|
||||||
},
|
},
|
||||||
"vitaminD": {
|
"vitaminD": {
|
||||||
"label": "D-vitamiin",
|
"label": "Витамин D",
|
||||||
"description": "Pending"
|
"description": "Ожидается"
|
||||||
},
|
},
|
||||||
"glucose": {
|
"glucose": {
|
||||||
"label": "Glükoos",
|
"label": "Глюкоза",
|
||||||
"description": "Pending"
|
"description": "Ожидается"
|
||||||
},
|
},
|
||||||
"alat": {
|
"alat": {
|
||||||
"label": "Alaniini aminotransferaas",
|
"label": "Аланинаминотрансфераза",
|
||||||
"description": "Pending"
|
"description": "Ожидается"
|
||||||
},
|
},
|
||||||
"ast": {
|
"ast": {
|
||||||
"label": "Aspartaadi aminotransferaas",
|
"label": "Аспартатаминотрансфераза",
|
||||||
"description": "Pending"
|
"description": "Ожидается"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,164 +1,197 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"pageTitle": "Home"
|
"pageTitle": "Обзор",
|
||||||
|
"headerTitle": "Обзор Tervisekassa {{companyName}}",
|
||||||
|
"healthDetails": "Данные о здоровье компании",
|
||||||
|
"membersSettingsButtonTitle": "Управление сотрудниками",
|
||||||
|
"membersSettingsButtonDescription": "Добавляйте, редактируйте или удаляйте сотрудников.",
|
||||||
|
"membersBillingButtonTitle": "Управление бюджетом",
|
||||||
|
"membersBillingButtonDescription": "Выберите, как распределять бюджет между сотрудниками."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"pageTitle": "Settings",
|
"pageTitle": "Настройки",
|
||||||
"pageDescription": "Manage your Company details",
|
"pageDescription": "Управление данными вашей компании",
|
||||||
"teamLogo": "Company Logo",
|
"teamLogo": "Логотип компании",
|
||||||
"teamLogoDescription": "Update your company's logo to make it easier to identify",
|
"teamLogoDescription": "Обновите логотип вашей компании для упрощения идентификации",
|
||||||
"teamName": "Company Name",
|
"teamName": "Название компании",
|
||||||
"teamNameDescription": "Update your company's name",
|
"teamNameDescription": "Обновите название вашей компании",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Опасная зона",
|
||||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
"dangerZoneDescription": "Этот раздел содержит действия, которые невозможно отменить"
|
||||||
},
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"pageTitle": "Members"
|
"pageTitle": "Сотрудники"
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"pageTitle": "Billing"
|
"pageTitle": "Биллинг"
|
||||||
},
|
},
|
||||||
"yourTeams": "Your Companies ({{teamsCount}})",
|
"benefitStatistics": {
|
||||||
"createTeam": "Create a Company",
|
"budget": {
|
||||||
"creatingTeam": "Creating Company...",
|
"title": "Баланс Tervisekassa компании",
|
||||||
"personalAccount": "Personal Account",
|
"balance": "Остаток бюджета {{balance}}",
|
||||||
"searchAccount": "Search Account...",
|
"volume": "Объем бюджета {{volume}}"
|
||||||
"membersTabLabel": "Members",
|
},
|
||||||
"memberName": "Name",
|
"data": {
|
||||||
"youLabel": "You",
|
"reservations": "{{value}} услуги",
|
||||||
"emailLabel": "Email",
|
"analysis": "Анализы",
|
||||||
"roleLabel": "Role",
|
"doctorsAndSpecialists": "Врачи и специалисты",
|
||||||
"primaryOwnerLabel": "Primary Admin",
|
"researches": "Исследования",
|
||||||
"joinedAtLabel": "Joined at",
|
"healthResearchPlans": "Пакеты медицинских исследований",
|
||||||
"invitedAtLabel": "Invited at",
|
"serviceUsage": "{{value}} использование услуг",
|
||||||
"inviteMembersPageSubheading": "Invite members to your Company",
|
"serviceSum": "Сумма услуг",
|
||||||
"createTeamModalHeading": "Create Company",
|
"eclinic": "Дигиклиника"
|
||||||
"createTeamModalDescription": "Create a new Company to manage your projects and members.",
|
}
|
||||||
"teamNameLabel": "Company Name",
|
},
|
||||||
"teamNameDescription": "Your company name should be unique and descriptive",
|
"healthDetails": {
|
||||||
"createTeamSubmitLabel": "Create Company",
|
"women": "Женщины",
|
||||||
"createTeamSuccess": "Company created successfully",
|
"men": "Мужчины",
|
||||||
"createTeamError": "Company not created. Please try again.",
|
"avgAge": "Средний возраст",
|
||||||
"createTeamLoading": "Creating company...",
|
"bmi": "ИМТ",
|
||||||
"settingsPageLabel": "General",
|
"cholesterol": "Общий холестерин",
|
||||||
"createTeamDropdownLabel": "New company",
|
"vitaminD": "Витамин D",
|
||||||
"changeRole": "Change Role",
|
"smokers": "Курящие"
|
||||||
"removeMember": "Remove from Account",
|
},
|
||||||
"inviteMembersSuccess": "Members invited successfully!",
|
"yourTeams": "Ваши компании ({{teamsCount}})",
|
||||||
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
"createTeam": "Создать компанию",
|
||||||
"inviteMembersLoading": "Inviting members...",
|
"creatingTeam": "Создание компании...",
|
||||||
"removeInviteButtonLabel": "Remove invite",
|
"personalAccount": "Личный аккаунт",
|
||||||
"addAnotherMemberButtonLabel": "Add another one",
|
"searchAccount": "Поиск аккаунта...",
|
||||||
"inviteMembersButtonLabel": "Send Invites",
|
"membersTabLabel": "Сотрудники",
|
||||||
"removeMemberModalHeading": "You are removing this user",
|
"memberName": "Имя",
|
||||||
"removeMemberModalDescription": "Remove this member from the company. They will no longer have access to the company.",
|
"youLabel": "Вы",
|
||||||
"removeMemberSuccessMessage": "Member removed successfully",
|
"emailLabel": "E-mail",
|
||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"roleLabel": "Роль",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
"primaryOwnerLabel": "Главный админ",
|
||||||
"removeMemberLoadingMessage": "Removing member...",
|
"joinedAtLabel": "Присоединился",
|
||||||
"removeMemberSubmitLabel": "Remove User from Company",
|
"invitedAtLabel": "Приглашен",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"inviteMembersPageSubheading": "Пригласите сотрудников в компанию",
|
||||||
"updateRole": "Update Role",
|
"createTeamModalHeading": "Создать компанию",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"createTeamModalDescription": "Создайте новую компанию для управления проектами и сотрудниками.",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"teamNameLabel": "Название компании",
|
||||||
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
"teamNameDescription": "Название вашей компании должно быть уникальным и описательным",
|
||||||
"updateMemberRoleModalHeading": "Update Member's Role",
|
"createTeamSubmitLabel": "Создать компанию",
|
||||||
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
|
"createTeamSuccess": "Компания успешно создана",
|
||||||
"roleMustBeDifferent": "Role must be different from the current one",
|
"createTeamError": "Компания не создана. Пожалуйста, попробуйте снова.",
|
||||||
"memberRoleInputLabel": "Member role",
|
"createTeamLoading": "Создание компании...",
|
||||||
"updateRoleDescription": "Pick a role for this member.",
|
"settingsPageLabel": "Основное",
|
||||||
"updateRoleSubmitLabel": "Update Role",
|
"createTeamDropdownLabel": "Новая компания",
|
||||||
"transferOwnership": "Transfer Ownership",
|
"changeRole": "Изменить роль",
|
||||||
"transferOwnershipDescription": "Transfer ownership of the company account to another member.",
|
"removeMember": "Удалить из аккаунта",
|
||||||
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
|
"inviteMembersSuccess": "Сотрудники успешно приглашены!",
|
||||||
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary admin of the company account.",
|
"inviteMembersError": "Произошла ошибка! Пожалуйста, попробуйте снова",
|
||||||
"deleteInvitation": "Delete Invitation",
|
"inviteMembersLoading": "Приглашение сотрудников...",
|
||||||
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
|
"removeInviteButtonLabel": "Удалить приглашение",
|
||||||
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
"addAnotherMemberButtonLabel": "Добавить ещё одного",
|
||||||
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
"inviteMembersButtonLabel": "Отправить приглашения",
|
||||||
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
"removeMemberModalHeading": "Вы удаляете этого пользователя",
|
||||||
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
"removeMemberModalDescription": "Удалите этого сотрудника из компании. Он больше не будет иметь доступ к компании.",
|
||||||
"transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to <b>{{ member }}</b>.",
|
"removeMemberSuccessMessage": "Сотрудник успешно удален",
|
||||||
"transferringOwnership": "Transferring ownership...",
|
"removeMemberErrorMessage": "Произошла ошибка. Пожалуйста, попробуйте снова",
|
||||||
"transferOwnershipSuccess": "Ownership successfully transferred",
|
"removeMemberErrorHeading": "Не удалось удалить выбранного сотрудника.",
|
||||||
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
"removeMemberLoadingMessage": "Удаление сотрудника...",
|
||||||
"deleteInviteSubmitLabel": "Delete Invite",
|
"removeMemberSubmitLabel": "Удалить пользователя из компании",
|
||||||
"youBadgeLabel": "You",
|
"chooseDifferentRoleError": "Роль совпадает с текущей",
|
||||||
"updateTeamLoadingMessage": "Updating Company...",
|
"updateRole": "Обновить роль",
|
||||||
"updateTeamSuccessMessage": "Company successfully updated",
|
"updateRoleLoadingMessage": "Обновление роли...",
|
||||||
"updateTeamErrorMessage": "Could not update Company. Please try again.",
|
"updateRoleSuccessMessage": "Роль успешно обновлена",
|
||||||
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
"updatingRoleErrorMessage": "Произошла ошибка. Пожалуйста, попробуйте снова.",
|
||||||
"teamNameInputLabel": "Company Name",
|
"updateMemberRoleModalHeading": "Обновить роль сотрудника",
|
||||||
"teamLogoInputHeading": "Upload your company's Logo",
|
"updateMemberRoleModalDescription": "Измените роль выбранного сотрудника. Роль определяет его права.",
|
||||||
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
"roleMustBeDifferent": "Роль должна отличаться от текущей",
|
||||||
"updateTeamSubmitLabel": "Update Company",
|
"memberRoleInputLabel": "Роль сотрудника",
|
||||||
"inviteMembersHeading": "Invite Members to your Company",
|
"updateRoleDescription": "Выберите роль для этого сотрудника.",
|
||||||
"inviteMembersDescription": "Invite member to your company by entering their email and role.",
|
"updateRoleSubmitLabel": "Обновить роль",
|
||||||
"emailPlaceholder": "member@email.com",
|
"transferOwnership": "Передача собственности",
|
||||||
"membersPageHeading": "Members",
|
"transferOwnershipDescription": "Передайте права владельца аккаунта компании другому сотруднику.",
|
||||||
"inviteMembersButton": "Invite Members",
|
"transferOwnershipInputLabel": "Пожалуйста, введите TRANSFER для подтверждения передачи собственности.",
|
||||||
"invitingMembers": "Inviting members...",
|
"transferOwnershipInputDescription": "Передав права собственности, вы больше не будете главным администратором компании.",
|
||||||
"inviteMembersSuccessMessage": "Members invited successfully",
|
"deleteInvitation": "Удалить приглашение",
|
||||||
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
|
"deleteInvitationDialogDescription": "Вы собираетесь удалить приглашение. Пользователь больше не сможет присоединиться к аккаунту компании.",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"deleteInviteSuccessMessage": "Приглашение успешно удалено",
|
||||||
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
|
"deleteInviteErrorMessage": "Приглашение не удалено. Пожалуйста, попробуйте снова.",
|
||||||
"noPendingInvites": "No pending invites found",
|
"deleteInviteLoadingMessage": "Удаление приглашения...",
|
||||||
"loadingMembers": "Loading members...",
|
"confirmDeletingMemberInvite": "Вы удаляете приглашение для <b>{{ email }}</b>",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your company's members.",
|
"transferOwnershipDisclaimer": "Вы передаете права собственности выбранного аккаунта компании <b>{{ member }}</b>.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited members.",
|
"transferringOwnership": "Передача собственности...",
|
||||||
"loadingInvitedMembers": "Loading invited members...",
|
"transferOwnershipSuccess": "Права собственности успешно переданы",
|
||||||
"invitedBadge": "Invited",
|
"transferOwnershipError": "Не удалось передать права собственности выбранному сотруднику. Пожалуйста, попробуйте снова.",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"deleteInviteSubmitLabel": "Удалить приглашение",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"youBadgeLabel": "Вы",
|
||||||
"dangerZone": "Danger Zone",
|
"updateTeamLoadingMessage": "Обновление компании...",
|
||||||
"dangerZoneSubheading": "Delete or leave your company",
|
"updateTeamSuccessMessage": "Компания успешно обновлена",
|
||||||
"deleteTeam": "Delete Company",
|
"updateTeamErrorMessage": "Не удалось обновить компанию. Пожалуйста, попробуйте снова.",
|
||||||
"deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.",
|
"updateLogoErrorMessage": "Не удалось обновить логотип. Пожалуйста, попробуйте снова.",
|
||||||
"deletingTeam": "Deleting company",
|
"teamNameInputLabel": "Название компании",
|
||||||
"deleteTeamModalHeading": "Deleting Company",
|
"teamLogoInputHeading": "Загрузите логотип вашей компании",
|
||||||
"deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.",
|
"teamLogoInputSubheading": "Выберите фото для загрузки в качестве логотипа компании.",
|
||||||
"deleteTeamInputField": "Type the name of the company to confirm",
|
"updateTeamSubmitLabel": "Обновить компанию",
|
||||||
"leaveTeam": "Leave Company",
|
"inviteMembersHeading": "Пригласить сотрудников в компанию",
|
||||||
"leavingTeamModalHeading": "Leaving Company",
|
"inviteMembersDescription": "Пригласите сотрудника в компанию, указав его email и роль.",
|
||||||
"leavingTeamModalDescription": "You are about to leave this company. You will no longer have access to it.",
|
"emailPlaceholder": "member@email.com",
|
||||||
"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.",
|
"membersPageHeading": "Сотрудники",
|
||||||
"deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.",
|
"inviteMembersButton": "Пригласить сотрудников",
|
||||||
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
"invitingMembers": "Приглашение сотрудников...",
|
||||||
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
"inviteMembersSuccessMessage": "Сотрудники успешно приглашены",
|
||||||
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
"inviteMembersErrorMessage": "Не удалось пригласить сотрудников. Пожалуйста, попробуйте снова.",
|
||||||
"searchMembersPlaceholder": "Search members",
|
"pendingInvitesHeading": "Ожидающие приглашения",
|
||||||
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
"pendingInvitesDescription": "Здесь можно управлять ожидающими приглашениями в вашу компанию.",
|
||||||
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
"noPendingInvites": "Нет ожидающих приглашений",
|
||||||
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
"loadingMembers": "Загрузка сотрудников...",
|
||||||
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
|
"loadMembersError": "Не удалось получить список сотрудников компании.",
|
||||||
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
|
"loadInvitedMembersError": "Не удалось получить список приглашённых сотрудников компании.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
|
"loadingInvitedMembers": "Загрузка приглашённых сотрудников...",
|
||||||
"searchInvitations": "Search Invitations",
|
"invitedBadge": "Приглашён",
|
||||||
"updateInvitation": "Update Invitation",
|
"duplicateInviteEmailError": "Вы уже добавили этот адрес электронной почты",
|
||||||
"removeInvitation": "Remove Invitation",
|
"invitingOwnAccountError": "Эй, это ваш email!",
|
||||||
"acceptInvitation": "Accept Invitation",
|
"dangerZone": "Опасная зона",
|
||||||
"renewInvitation": "Renew Invitation",
|
"dangerZoneSubheading": "Удалить или покинуть компанию",
|
||||||
"resendInvitation": "Resend Invitation",
|
"deleteTeam": "Удалить компанию",
|
||||||
"expiresAtLabel": "Expires at",
|
"deleteTeamDescription": "Это действие невозможно отменить. Все данные, связанные с этой компанией, будут удалены.",
|
||||||
"expired": "Expired",
|
"deletingTeam": "Удаление компании...",
|
||||||
"active": "Active",
|
"deleteTeamModalHeading": "Удаление компании",
|
||||||
"inviteStatus": "Status",
|
"deletingTeamDescription": "Вы собираетесь удалить компанию {{ teamName }}. Это действие невозможно отменить.",
|
||||||
"inviteNotFoundOrExpired": "Invite not found or expired",
|
"deleteTeamInputField": "Введите название компании для подтверждения",
|
||||||
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company admin to renew the invite.",
|
"leaveTeam": "Покинуть компанию",
|
||||||
"backToHome": "Back to Home",
|
"leavingTeamModalHeading": "Покидание компании",
|
||||||
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
|
"leavingTeamModalDescription": "Вы собираетесь покинуть эту компанию. У вас больше не будет к ней доступа.",
|
||||||
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
"leaveTeamDescription": "Нажмите кнопку ниже, чтобы покинуть компанию. Помните, что вы больше не будете иметь к ней доступа и вам потребуется повторное приглашение для присоединения.",
|
||||||
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
|
"deleteTeamDisclaimer": "Вы удаляете компанию {{ teamName }}. Это действие невозможно отменить.",
|
||||||
"signInWithDifferentAccount": "Sign in with a different account",
|
"leaveTeamDisclaimer": "Вы покидаете компанию {{ teamName }}. У вас больше не будет к ней доступа.",
|
||||||
"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.",
|
"deleteTeamErrorHeading": "Не удалось удалить вашу компанию.",
|
||||||
"acceptInvitationHeading": "Accept Invitation to join {{accountName}}",
|
"leaveTeamErrorHeading": "Не удалось покинуть вашу компанию.",
|
||||||
"acceptInvitationDescription": "You have been invited to join the company {{accountName}}. If you wish to accept the invitation, please click the button below.",
|
"searchMembersPlaceholder": "Поиск сотрудников",
|
||||||
"continueAs": "Continue as {{email}}",
|
"createTeamErrorHeading": "Не удалось создать вашу компанию.",
|
||||||
"joinTeamAccount": "Join Company",
|
"createTeamErrorMessage": "Произошла ошибка при создании компании. Пожалуйста, попробуйте снова.",
|
||||||
"joiningTeam": "Joining company...",
|
"transferTeamErrorHeading": "Не удалось передать права собственности аккаунта компании.",
|
||||||
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
"transferTeamErrorMessage": "Произошла ошибка при передаче прав собственности аккаунта компании. Пожалуйста, попробуйте снова.",
|
||||||
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
"updateRoleErrorHeading": "Не удалось обновить роль выбранного сотрудника.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"updateRoleErrorMessage": "Произошла ошибка при обновлении роли выбранного сотрудника. Пожалуйста, попробуйте снова.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.",
|
"searchInvitations": "Поиск приглашений",
|
||||||
"personalCode": "Личный код"
|
"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-down: accordion-down 0.2s ease-out;
|
||||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
|
--breakpoint-2xs: 36rem;
|
||||||
--breakpoint-xs: 48rem;
|
--breakpoint-xs: 48rem;
|
||||||
--breakpoint-sm: 64rem;
|
--breakpoint-sm: 64rem;
|
||||||
--breakpoint-md: 70rem;
|
--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