4 Commits

Author SHA1 Message Date
bc61db07b2 test2 2025-09-04 01:21:42 +03:00
3d58e5aa84 try to display price before adding to cart 2025-09-03 23:04:09 +03:00
8ecca096f2 fix adding to cart loading 2025-09-03 23:03:00 +03:00
84c8dcc792 env test 2025-09-03 14:24:56 +03:00
100 changed files with 4032 additions and 4602 deletions

2
.env
View File

@@ -13,7 +13,7 @@ NEXT_PUBLIC_THEME_COLOR="#ffffff"
NEXT_PUBLIC_THEME_COLOR_DARK="#0a0a0a"
# AUTH
NEXT_PUBLIC_AUTH_PASSWORD=true
NEXT_PUBLIC_AUTH_PASSWORD=false
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
NEXT_PUBLIC_CAPTCHA_SITE_KEY=

View File

@@ -36,3 +36,57 @@ MONTONIO_API_URL=https://sandbox-stargate.montonio.com
# JOBS
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
# MEDIPOST
MEDIPOST_URL=https://meditest.medisoft.ee:7443/Medipost/MedipostServlet
MEDIPOST_USER=trvurgtst
MEDIPOST_PASSWORD=SRB48HZMV
MEDIPOST_RECIPIENT=trvurgtst
MEDIPOST_MESSAGE_SENDER=trvurgtst
MEDIPOST_URL=https://medipost2.medisoft.ee:8443/Medipost/MedipostServlet
MEDIPOST_USER=medreport
MEDIPOST_PASSWORD=85MXFFDB7
MEDIPOST_RECIPIENT=HTI
MEDIPOST_MESSAGE_SENDER=medreport
### TEST.MEDREPORT.ee ###
DB_PASSWORD=T#u-$M7%RjbA@L@
#### MEDUSA
MEDUSA_BACKEND_URL=https://backoffice-test.medreport.ee
MEDUSA_BACKEND_PUBLIC_URL=https://backoffice-test.medreport.ee
MEDUSA_SECRET_API_KEY=sk_fdb1808fbabf62979cc46316aa997378ffbb87882883e8f5c3ee47cee39dcac5
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_827a2ab863021cb67993f1d81078f81bfce4b4e0da642d8c0f5398ded9d8fd32
#### SUPABASE
NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0NjUyODEyMywiZXhwIjoyMDYyMTA0MTIzfQ.KVcnkZ21Pd0XkJho23dZqFHawVTLQqfvF7l2RxsELLk
#######
### LOCAL ###
#### MEDUSA
MEDUSA_BACKEND_URL=http://localhost:9000
MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000
#MEDUSA_BACKEND_URL=http://5.181.51.38:9000
#MEDUSA_BACKEND_PUBLIC_URL=http://5.181.51.38:9000
MEDUSA_SECRET_API_KEY=sk_b332d525212ab4078ef73fb2b8232c3beebccc4a460e2c7abf6e187a458d60cf
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_e23a820689a07d55aa0a0ad187268559f5d6288ecb0768ff4520516285bdef84
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_0ec86252438b38ce18d5601f7877e4395d7e0a6afa8687dfea8d37af33015633
#### SUPABASE
NEXT_PUBLIC_SUPABASE_URL=http://5.181.51.38:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
#######
SUPABASE_AUTH_CLIENT_ID=supabase
SUPABASE_AUTH_KEYCLOAK_SECRET=Gl394GjizClhQl06KFeoFyZ7ZbPamG5I
SUPABASE_AUTH_KEYCLOAK_URL=http://localhost:8585/realms/medreport-sandbox
SUPABASE_AUTH_KEYCLOAK_CALLBACK_URL=http://localhost:3000/auth/callback

View File

@@ -6,10 +6,10 @@
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
# SUPABASE
# NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
# # MONTONIO
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
# MONTONIO
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead

View File

@@ -6,10 +6,10 @@
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
# SUPABASE
# NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
NEXT_PUBLIC_SUPABASE_URL=https://kaldvociniytdbbcxvqk.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImthbGR2b2Npbml5dGRiYmN4dnFrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTYzNjQ5OTYsImV4cCI6MjA3MTk0MDk5Nn0.eixihH2KGkJZolY9FiQDicJOo2kxvXrSe6gGUCrkLo0
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
# # MONTONIO
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
# MONTONIO
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead

View File

@@ -19,4 +19,6 @@ EMAIL_PASSWORD=password
# STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51K9cWKI1i3VnbZTq2HGstY2S8wt3peF1MOqPXFO4LR8ln2QgS7GxL8XyKaKLvn7iFHeqAnvdDw0o48qN7rrwwcHU00jOtKhjsf
CONTACT_EMAIL=test@makerkit.dev
CONTACT_EMAIL=test@makerkit.dev
SUPABASE_AUTH_KEYCLOAK_URL=https://keycloak.medreport.ee/realms/medreport-prod

View File

@@ -6,67 +6,29 @@ WORKDIR /app
RUN npm install -g pnpm@9
RUN npm install -g dotenv-cli
ARG MEDUSA_BACKEND_URL
ARG NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
ARG SUPABASE_SERVICE_ROLE_KEY
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ARG NEXT_PUBLIC_SITE_URL
ARG NEXT_PUBLIC_MONTONIO_ACCESS_KEY
ARG JOBS_API_TOKEN
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages packages
COPY tooling tooling
ENV SKIP_SUPABASE_CLI=true
RUN pnpm install --frozen-lockfile
COPY .env .env
#COPY .env.staging .env.staging
RUN echo "# PRODUCTION BUILD ENVIRONMENT VARIABLES" > .env.production && \
echo "" >> .env.production && \
echo "# MEDUSA" >> .env.production && \
echo "MEDUSA_BACKEND_URL=${MEDUSA_BACKEND_URL}" >> .env.production && \
echo "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=${NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}" >> .env.production && \
echo "" >> .env.production && \
echo "# SUPABASE" >> .env.production && \
echo "SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}" >> .env.production && \
echo "NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}" >> .env.production && \
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}" >> .env.production && \
echo "" >> .env.production && \
echo "# SITE" >> .env.production && \
echo "NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}" >> .env.production && \
echo "" >> .env.production && \
echo "# MONTONIO" >> .env.production && \
echo "NEXT_PUBLIC_MONTONIO_ACCESS_KEY=${NEXT_PUBLIC_MONTONIO_ACCESS_KEY}" >> .env.production && \
echo "" >> .env.production && \
echo "# JOBS" >> .env.production && \
echo "JOBS_API_TOKEN=${JOBS_API_TOKEN}" >> .env.local
COPY .env.production .env.production
COPY .env.staging .env.staging
# Load env file and echo a specific variable
# RUN dotenv -e .env -- printenv | grep 'SUPABASE' || true
RUN pnpm install --frozen-lockfile
COPY . .
ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV ENABLE_REACT_COMPILER=false
RUN echo "🔍 Checking environment files before build:" && \
ls -la .env* && \
echo "🔍 Contents of .env.production:" && \
cat .env.production > /tmp/env-debug.log && \
cat /tmp/env-debug.log && \
echo "🔍 Contents of .env.local:" && \
cat .env.local >> /tmp/env-debug.log && \
cat .env.local && \
echo "🔍 Parameter Store variables received:" && \
printenv | grep -E "MEDUSA_BACKEND_URL|NEXT_PUBLIC_SUPABASE_URL|NEXT_PUBLIC_SITE_URL|JOBS_API_TOKEN" | tee -a /tmp/env-debug.log && \
echo "🔍 Starting Next.js build:" && \
pnpm build --debug
RUN set -a \
&& . .env \
&& . .env.production \
&& . .env.staging \
&& set +a \
&& node check-env.js \
&& pnpm build
# --- Stage 2: Runtime ---
FROM node:20-alpine
@@ -75,19 +37,16 @@ ARG APP_ENV=production
WORKDIR /app
COPY --from=builder /app ./
COPY --from=builder /app/.env.${APP_ENV} .env.production
COPY --from=builder /app/.env.local .env.local
RUN cp ".env.${APP_ENV}" .env.local
RUN npm install -g pnpm@9 \
&& pnpm install --prod --frozen-lockfile
&& pnpm install --prod --frozen-lockfile
ENV NODE_ENV=production
# 🔍 Optional: Log key envs for debug
RUN echo "📄 .env.local contents:" && cat .env.local \
&& echo "📄 .env.production contents:" && cat .env.production \
RUN echo "📄 .env contents:" && cat .env.local \
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true

View File

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

View File

@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
import { retrieveOrder } from "@lib/data/orders";
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
@@ -25,7 +25,7 @@ export const POST = async (request: NextRequest) => {
try {
const medusaOrder = await retrieveOrder(medusaOrderId);
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
console.info("Successfully sent order to medipost");
return NextResponse.json({

View File

@@ -3,7 +3,7 @@ import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { getOrderedAnalysisIds } from "~/lib/services/medipost.service";
import { getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
const medusaOrder = await retrieveOrder(medusaOrderId)
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
const idsToSend = orderedAnalysisElementsIds;
@@ -35,8 +35,8 @@ export async function POST(request: NextRequest) {
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: idsToSend.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: [],
orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at),
});

View File

@@ -3,7 +3,7 @@ import { getOrder } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { createMedipostActionLog, getOrderedAnalysisIds } from "~/lib/services/medipost.service";
import { createMedipostActionLog, getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
export async function POST(request: Request) {
// const isDev = process.env.NODE_ENV === 'development';
@@ -11,15 +11,16 @@ export async function POST(request: Request) {
// return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 });
// }
const { medusaOrderId } = await request.json();
const { medusaOrderId, maxItems = null } = await request.json();
const medusaOrder = await retrieveOrder(medusaOrderId)
const medreportOrder = await getOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} (${maxItems ?? 'all'}) ordered analysis elements`);
const idsToSend = typeof maxItems === 'number' ? orderedAnalysisElementsIds.slice(0, maxItems) : orderedAnalysisElementsIds;
const messageXml = await composeOrderTestResponseXML({
person: {
idCode: account.personal_code!,
@@ -27,8 +28,8 @@ export async function POST(request: Request) {
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: [],
orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at),
});

View File

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

View File

@@ -8,7 +8,7 @@ import { listProductTypes } from "@lib/data/products";
import { placeOrder, retrieveCart } from "@lib/data/cart";
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
import { createOrder } from '~/lib/services/order.service';
import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
import { createNotificationsApi } from '@kit/notifications/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { AccountWithParams } from '@kit/accounts/api';
@@ -20,12 +20,12 @@ const env = () => z
.object({
emailSender: z
.string({
error: 'EMAIL_SENDER is required',
required_error: 'EMAIL_SENDER is required',
})
.min(1),
siteUrl: z
.string({
error: 'NEXT_PUBLIC_SITE_URL is required',
required_error: 'NEXT_PUBLIC_SITE_URL is required',
})
.min(1),
})
@@ -114,7 +114,7 @@ export async function processMontonioCallback(orderToken: string) {
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
const { productTypes } = await listProductTypes();

View File

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

View File

@@ -1,128 +0,0 @@
'use client';
import Link from 'next/link';
import { BlendingModeIcon } from '@radix-ui/react-icons';
import {
Droplets,
} from 'lucide-react';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardHeader,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
const dummyRecommendations = [
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-cyan/10 text-cyan',
title: 'Kolesterooli kontroll',
description: 'HDL-kolestrool',
tooltipContent: 'Selgitus',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-primary/10 text-primary',
title: 'Kolesterooli kontroll',
tooltipContent: 'Selgitus',
description: 'LDL-Kolesterool',
buttonText: 'Broneeri',
href: '/home/booking',
},
{
icon: <Droplets />,
color: 'bg-destructive/10 text-destructive',
title: 'Vererõhu kontroll',
tooltipContent: 'Selgitus',
description: 'Score-Risk 2',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
];
export default function DashboardRecommendations() {
return (
<Card>
<CardHeader className="items-start">
<h4>
<Trans i18nKey="dashboard:recommendedForYou" />
</h4>
</CardHeader>
<CardContent className="space-y-6">
{dummyRecommendations.map(
(
{
icon,
color,
title,
description,
tooltipContent,
price,
buttonText,
href,
},
index,
) => {
return (
<div
className="flex w-full justify-between gap-3 overflow-scroll"
key={index}
>
<div className="mr-4 flex min-w-fit flex-row items-center gap-4">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
color,
)}
>
{icon}
</div>
<div className="min-w-fit">
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
{title}
<InfoTooltip content={tooltipContent} />
</div>
<p className="text-muted-foreground text-sm">
{description}
</p>
</div>
</div>
<div className="grid w-36 min-w-fit auto-rows-fr grid-cols-2 items-center gap-4">
<p className="text-sm font-medium"> {price}</p>
{href ? (
<Link href={href}>
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
</Link>
) : (
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
)}
</div>
</div>
);
},
)}
</CardContent>
</Card>
);
}

View File

@@ -7,41 +7,33 @@ import { Database } from '@/packages/supabase/src/database.types';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import {
Activity,
ChevronRight,
Clock9,
Droplets,
Pill,
Scale,
TrendingUp,
User,
} from 'lucide-react';
import { pathsConfig } from '@kit/shared/config';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { getPersonParameters } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardProps,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { isNil } from 'lodash';
import { BmiCategory } from '~/lib/types/bmi';
import {
bmiFromMetric,
getBmiBackgroundColor,
getBmiStatus,
} from '~/lib/utils';
import DashboardRecommendations from './dashboard-recommendations';
const getCardVariant = (isSuccess: boolean | null): CardProps['variant'] => {
if (isSuccess === null) return 'default';
if (isSuccess) return 'gradient-success';
return 'gradient-destructive';
};
const cards = ({
gender,
@@ -49,91 +41,111 @@ const cards = ({
height,
weight,
bmiStatus,
smoking,
}: {
gender?: string;
age?: number;
height?: number | null;
weight?: number | null;
bmiStatus: BmiCategory | null;
smoking?: boolean | null;
}) => [
{
title: 'dashboard:gender',
description: gender ?? 'dashboard:male',
icon: <User />,
iconBg: 'bg-success',
},
{
title: 'dashboard:age',
description: age ? `${age}` : '-',
icon: <Clock9 />,
iconBg: 'bg-success',
},
{
title: 'dashboard:height',
description: height ? `${height}cm` : '-',
icon: <RulerHorizontalIcon className="size-4" />,
iconBg: 'bg-success',
},
{
title: 'dashboard:weight',
description: weight ? `${weight}kg` : '-',
icon: <Scale />,
iconBg: 'bg-success',
},
{
title: 'dashboard:bmi',
description: bmiFromMetric(weight || 0, height || 0).toString(),
icon: <TrendingUp />,
iconBg: getBmiBackgroundColor(bmiStatus),
},
{
title: 'dashboard:bloodPressure',
description: '-',
icon: <Activity />,
iconBg: 'bg-warning',
},
{
title: 'dashboard:cholesterol',
description: '-',
icon: <BlendingModeIcon className="size-4" />,
iconBg: 'bg-destructive',
},
{
title: 'dashboard:ldlCholesterol',
description: '-',
icon: <Pill />,
iconBg: 'bg-warning',
},
// {
// title: 'Score 2',
// description: 'Normis',
// icon: <LineChart />,
// iconBg: 'bg-success',
// },
{
title: 'dashboard:smoking',
description:
isNil(smoking)
? 'dashboard:respondToQuestion'
: !!smoking
? 'common:yes'
: 'common:no',
descriptionColor: 'text-primary',
icon:
isNil(smoking) ? (
<Link href={pathsConfig.app.personalAccountSettings}>
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
) : null,
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
},
];
{
title: 'dashboard:gender',
description: gender ?? 'dashboard:male',
icon: <User />,
iconBg: 'bg-success',
},
{
title: 'dashboard:age',
description: age ? `${age}` : '-',
icon: <Clock9 />,
iconBg: 'bg-success',
},
{
title: 'dashboard:height',
description: height ? `${height}cm` : '-',
icon: <RulerHorizontalIcon className="size-4" />,
iconBg: 'bg-success',
},
{
title: 'dashboard:weight',
description: weight ? `${weight}kg` : '-',
icon: <Scale />,
iconBg: 'bg-success',
},
{
title: 'dashboard:bmi',
description: bmiFromMetric(weight || 0, height || 0).toString(),
icon: <TrendingUp />,
iconBg: getBmiBackgroundColor(bmiStatus),
},
{
title: 'dashboard:bloodPressure',
description: '-',
icon: <Activity />,
iconBg: 'bg-warning',
},
{
title: 'dashboard:cholesterol',
description: '-',
icon: <BlendingModeIcon className="size-4" />,
iconBg: 'bg-destructive',
},
{
title: 'dashboard:ldlCholesterol',
description: '-',
icon: <Pill />,
iconBg: 'bg-warning',
},
// {
// title: 'Score 2',
// description: 'Normis',
// icon: <LineChart />,
// iconBg: 'bg-success',
// },
// {
// title: 'dashboard:smoking',
// description: 'dashboard:respondToQuestion',
// descriptionColor: 'text-primary',
// icon: (
// <Button size="icon" variant="outline" className="px-2 text-black">
// <ChevronRight className="size-4 stroke-2" />
// </Button>
// ),
// cardVariant: 'gradient-success' as CardProps['variant'],
// },
];
const IS_SHOWN_RECOMMENDATIONS = false as boolean;
const dummyRecommendations = [
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-cyan/10 text-cyan',
title: 'Kolesterooli kontroll',
description: 'HDL-kolestrool',
tooltipContent: 'Selgitus',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
{
icon: <BlendingModeIcon className="size-4" />,
color: 'bg-primary/10 text-primary',
title: 'Kolesterooli kontroll',
tooltipContent: 'Selgitus',
description: 'LDL-Kolesterool',
buttonText: 'Broneeri',
href: '/home/booking',
},
{
icon: <Droplets />,
color: 'bg-destructive/10 text-destructive',
title: 'Vererõhu kontroll',
tooltipContent: 'Selgitus',
description: 'Score-Risk 2',
price: '20,00 €',
buttonText: 'Telli',
href: '/home/booking',
},
];
export default function Dashboard({
account,
@@ -148,8 +160,8 @@ export default function Dashboard({
const params = getPersonParameters(account.personal_code!);
const bmiStatus = getBmiStatus(bmiThresholds, {
age: params?.age || 0,
height: account.accountParams?.height || 0,
weight: account.accountParams?.weight || 0,
height: account.account_params?.[0]?.height || 0,
weight: account.account_params?.[0]?.weight || 0,
});
return (
@@ -158,22 +170,21 @@ export default function Dashboard({
{cards({
gender: params?.gender,
age: params?.age,
height: account.accountParams?.height,
weight: account.accountParams?.weight,
height: account.account_params?.[0]?.height,
weight: account.account_params?.[0]?.weight,
bmiStatus,
smoking: account.accountParams?.isSmoker,
}).map(
({
title,
description,
icon,
iconBg,
cardVariant,
// cardVariant,
// descriptionColor,
}) => (
<Card
key={title}
variant={cardVariant}
// variant={cardVariant}
className="flex flex-col justify-between"
>
<CardHeader className="items-end-safe">
@@ -200,7 +211,79 @@ export default function Dashboard({
),
)}
</div>
{IS_SHOWN_RECOMMENDATIONS && <DashboardRecommendations />}
<Card>
<CardHeader className="items-start">
<h4>
<Trans i18nKey="dashboard:recommendedForYou" />
</h4>
</CardHeader>
<CardContent className="space-y-6">
{dummyRecommendations.map(
(
{
icon,
color,
title,
description,
tooltipContent,
price,
buttonText,
href,
},
index,
) => {
return (
<div
className="flex w-full justify-between gap-3 overflow-scroll"
key={index}
>
<div className="mr-4 flex min-w-fit flex-row items-center gap-4">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
color,
)}
>
{icon}
</div>
<div className="min-w-fit">
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
{title}
<InfoTooltip content={tooltipContent} />
</div>
<p className="text-muted-foreground text-sm">
{description}
</p>
</div>
</div>
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4 min-w-fit">
<p className="text-sm font-medium"> {price}</p>
{href ? (
<Link href={href}>
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
</Link>
) : (
<Button
size="sm"
variant="secondary"
className="w-full min-w-fit"
>
{buttonText}
</Button>
)}
</div>
</div>
);
},
)}
</CardContent>
</Card>
</>
);
}

View File

@@ -31,8 +31,7 @@ export async function HomeMenuNavigation(props: {
})
: 0;
const cartQuantityTotal =
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const cartQuantityTotal = props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const hasCartItems = cartQuantityTotal > 0;
return (
@@ -40,18 +39,15 @@ export async function HomeMenuNavigation(props: {
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
<AppLogo href={pathsConfig.app.home} />
</div>
{/* TODO: add search functionality */}
{/* <Search
<Search
className="flex grow"
startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />}
/> */}
/>
<div className="flex items-center justify-end gap-3">
{/* TODO: add wallet functionality
<Card className="px-6 py-2">
<span> {Number(0).toFixed(2).replace('.', ',')}</span>
</Card>
*/}
{hasCartItems && (
<Button
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
@@ -60,7 +56,7 @@ export async function HomeMenuNavigation(props: {
<span className="flex items-center text-nowrap">{totalValue}</span>
</Button>
)}
<Link href={pathsConfig.app.cart}>
<Link href="/home/cart">
<Button
variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -105,18 +105,12 @@ export const createMedusaSyncSuccessEntry = async () => {
});
}
export async function getAnalyses({ ids, originalIds }: { ids?: number[], originalIds?: string[] }): Promise<AnalysesWithGroupsAndElements> {
const query = getSupabaseServerAdminClient()
export async function getAnalyses({ ids }: { ids: number[] }): Promise<AnalysesWithGroupsAndElements> {
const { data } = await getSupabaseServerAdminClient()
.schema('medreport')
.from('analyses')
.select(`*, analysis_elements(*, analysis_groups(*))`);
if (Array.isArray(ids)) {
query.in('id', ids);
}
if (Array.isArray(originalIds)) {
query.in('analysis_id_original', originalIds);
}
const { data } = await query.throwOnError();
.select(`*, analysis_elements(*, analysis_groups(*))`)
.in('id', ids);
return data as unknown as AnalysesWithGroupsAndElements;
}

View File

@@ -666,7 +666,7 @@ async function syncPrivateMessage({
unit: element.Mootyhik ?? null,
original_response_element: element,
analysis_name: element.UuringNimi || element.KNimetus,
comment: element.UuringuKommentaar ?? '',
comment: element.UuringuKommentaar
})),
);
}
@@ -715,7 +715,7 @@ export async function sendOrderToMedipost({
orderedAnalysisElements,
}: {
medusaOrderId: string;
orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[];
orderedAnalysisElements: { analysisElementId: number }[];
}) {
const medreportOrder = await getOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
@@ -727,8 +727,8 @@ export async function sendOrderToMedipost({
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: [],
orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at),
comment: '',
@@ -784,13 +784,12 @@ export async function sendOrderToMedipost({
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
}
export async function getOrderedAnalysisIds({
export async function getOrderedAnalysisElementsIds({
medusaOrder,
}: {
medusaOrder: StoreOrder;
}): Promise<{
analysisElementId?: number;
analysisId?: number;
analysisElementId: number;
}[]> {
const countryCodes = await listRegions();
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
@@ -803,14 +802,6 @@ export async function getOrderedAnalysisIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
}
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
const originalIds = (medusaOrder?.items ?? [])
.map((a) => a.product?.metadata?.analysisIdOriginal)
.filter((a) => typeof a === 'string') as string[];
const analyses = await getAnalyses({ originalIds });
return analyses.map(({ id }) => ({ analysisId: id }));
}
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
@@ -850,13 +841,12 @@ export async function getOrderedAnalysisIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
}
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
const [analysisPackageElements, orderedAnalysisElements] = await Promise.all([
getOrderedAnalysisPackages(medusaOrder),
getOrderedAnalysisElements(medusaOrder),
getOrderedAnalyses(medusaOrder),
]);
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
return [...analysisPackageElements, ...orderedAnalysisElements];
}
export async function createMedipostActionLog({

View File

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

View File

@@ -10,7 +10,7 @@ export async function createOrder({
orderedAnalysisElements,
}: {
medusaOrder: StoreOrder;
orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[];
orderedAnalysisElements: { analysisElementId: number }[];
}) {
const supabase = getSupabaseServerClient();
@@ -21,8 +21,8 @@ export async function createOrder({
const orderResult = await supabase.schema('medreport')
.from('analysis_orders')
.insert({
analysis_element_ids: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
analysis_ids: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
analysis_element_ids: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
analysis_ids: [],
status: 'QUEUED',
user_id: user.id,
medusa_order_id: medusaOrder.id,

View File

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

View File

@@ -50,7 +50,7 @@ const config = {
},
experimental: {
mdxRs: true,
reactCompiler: false,
reactCompiler: ENABLE_REACT_COMPILER,
optimizePackageImports: [
'recharts',
'lucide-react',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,6 +102,7 @@ export const OauthProviders: React.FC<{
redirectTo,
queryParams: props.queryParams,
scopes,
skipBrowserRedirect: false,
},
} satisfies SignInWithOAuthCredentials;

View File

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

View File

@@ -272,12 +272,7 @@ export async function medusaLoginOrRegister(credentials: {
password,
});
await setAuthToken(token as string);
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
await transferCart();
const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag);
@@ -312,12 +307,7 @@ export async function medusaLoginOrRegister(credentials: {
const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag);
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
await transferCart();
const customer = await retrieveCustomer();
if (!customer) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,10 +4,12 @@ import type { User } from '@supabase/supabase-js';
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
import { ApplicationRole } from '@kit/accounts/types/accounts';
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import { useUser } from '@kit/supabase/hooks/use-user';
import { pathsConfig, featureFlagsConfig } from '@kit/shared/config';
const paths = {
home: pathsConfig.app.home,
admin: pathsConfig.app.admin,

View File

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

View File

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

View File

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

View File

@@ -6,30 +6,32 @@ const AppConfigSchema = z
.object({
name: z
.string({
error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
description: `This is the name of your SaaS. Ex. "Makerkit"`,
required_error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
})
.describe(`This is the name of your SaaS. Ex. "Makerkit"`)
.min(1),
title: z
.string({
error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
description: `This is the default title tag of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
})
.describe(`This is the default title tag of your SaaS.`)
.min(1),
description: z.string({
error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`,
})
.describe(`This is the default description of your SaaS.`),
url: z.url({
error: (issue) => issue.input === undefined
? "Please provide the variable NEXT_PUBLIC_SITE_URL"
: `You are deploying a production build but have entered a NEXT_PUBLIC_SITE_URL variable using http instead of https. It is very likely that you have set the incorrect URL. The build will now fail to prevent you from from deploying a faulty configuration. Please provide the variable NEXT_PUBLIC_SITE_URL with a valid URL, such as: 'https://example.com'`
description: `This is the default description of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`,
}),
url: z
.string({
required_error: `Please provide the variable NEXT_PUBLIC_SITE_URL`,
})
.url({
message: `You are deploying a production build but have entered a NEXT_PUBLIC_SITE_URL variable using http instead of https. It is very likely that you have set the incorrect URL. The build will now fail to prevent you from from deploying a faulty configuration. Please provide the variable NEXT_PUBLIC_SITE_URL with a valid URL, such as: 'https://example.com'`,
}),
locale: z
.string({
error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
description: `This is the default locale of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
})
.describe(`This is the default locale of your SaaS.`)
.default('en'),
theme: z.enum(['light', 'dark', 'system']),
production: z.boolean(),

View File

@@ -6,14 +6,22 @@ const providers: z.ZodType<Provider> = getProviders();
const AuthConfigSchema = z.object({
captchaTokenSiteKey: z
.string().describe('The reCAPTCHA site key.')
.string({
description: 'The reCAPTCHA site key.',
})
.optional(),
displayTermsCheckbox: z
.boolean().describe('Whether to display the terms checkbox during sign-up.')
.boolean({
description: 'Whether to display the terms checkbox during sign-up.',
})
.optional(),
providers: z.object({
password: z.boolean().describe('Enable password authentication.'),
magicLink: z.boolean().describe('Enable magic link authentication.'),
password: z.boolean({
description: 'Enable password authentication.',
}),
magicLink: z.boolean({
description: 'Enable magic link authentication.',
}),
oAuth: providers.array(),
}),
});
@@ -32,7 +40,7 @@ const authConfig = AuthConfigSchema.parse({
providers: {
password: process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true',
magicLink: process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true',
oAuth: ['google'],
oAuth: ['keycloak'],
},
} satisfies z.infer<typeof AuthConfigSchema>);

View File

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

View File

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

View File

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

View File

@@ -10,5 +10,11 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
export function getSupabaseBrowserClient<GenericSchema = Database>() {
const keys = getSupabaseClientKeys();
return createBrowserClient<GenericSchema>(keys.url, keys.anonKey);
return createBrowserClient<GenericSchema>(keys.url, keys.anonKey, {
auth: {
flowType: 'pkce',
autoRefreshToken: true,
persistSession: true,
},
});
}

View File

@@ -20,6 +20,11 @@ export function createMiddlewareClient<GenericSchema = Database>(
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
auth: {
flowType: 'pkce',
autoRefreshToken: true,
persistSession: true,
},
cookies: {
getAll() {
return request.cookies.getAll();

View File

@@ -15,6 +15,11 @@ export function getSupabaseServerClient<GenericSchema = Database>() {
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
auth: {
flowType: 'pkce',
autoRefreshToken: true,
persistSession: true,
},
cookies: {
async getAll() {
const cookieStore = await cookies();

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ export function useUser(initialData?: User | null) {
// this is most likely a session error or the user is not logged in
if (response.error) {
return null;
return undefined;
}
if (response.data?.user) {

View File

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

3775
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{
"recentlyCheckedDescription": "Here are your most important health indicators.",
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
"respondToQuestion": "Respond",
"gender": "Gender",
"male": "Male",

View File

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

View File

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

View File

@@ -1,90 +1,90 @@
{
"signUpHeading": "Loo konto",
"signUp": "Loo konto",
"signUpSubheading": "Täida allolev vorm, et luua konto.",
"signInHeading": "Logi oma kontole sisse",
"signInSubheading": "Tere tulemast tagasi! Palun sisesta oma andmed",
"signIn": "Logi sisse",
"getStarted": "Alusta",
"updatePassword": "Uuenda parooli",
"signOut": "Logi välja",
"signingIn": "Sisselogimine...",
"signingUp": "Registreerimine...",
"doNotHaveAccountYet": "Kas sul pole veel kontot?",
"alreadyHaveAnAccount": "Kas sul on juba konto?",
"signUpToAcceptInvite": "Palun logi sisse/registreeru, et kutse vastu võtta",
"clickToAcceptAs": "Klõpsa alloleval nupul, et võtta kutse vastu kui <b>{{email}}</b>",
"acceptInvite": "Võta kutse vastu",
"acceptingInvite": "Kutset vastu võttes...",
"acceptInviteSuccess": "Kutse on edukalt vastu võetud",
"acceptInviteError": "Kutse vastuvõtmisel tekkis viga",
"acceptInviteWithDifferentAccount": "Soovid kutse vastu võtta teise kontoga?",
"alreadyHaveAccountStatement": "Mul on juba konto, ma tahan sisse logida",
"doNotHaveAccountStatement": "Mul pole kontot, ma tahan registreeruda",
"signInWithProvider": "Logi sisse teenusega {{provider}}",
"signInWithPhoneNumber": "Logi sisse telefoninumbriga",
"signInWithEmail": "Logi sisse e-posti aadressiga",
"signUpWithEmail": "Registreeru e-posti aadressiga",
"passwordHint": "Veendu, et see oleks vähemalt 8 tähemärki pikk",
"repeatPasswordHint": "Sisesta oma parool uuesti",
"repeatPassword": "Korda parooli",
"passwordForgottenQuestion": "Unustasid parooli?",
"passwordResetLabel": "Taasta parool",
"passwordResetSubheading": "Sisesta oma e-posti aadress alla. Saadame sulle lingi parooli lähtestamiseks.",
"passwordResetSuccessMessage": "Kontrolli oma postkasti! Saatsime sulle parooli lähtestamise lingi.",
"passwordRecoveredQuestion": "Kas parool on taastatud?",
"passwordLengthError": "Palun sisesta parool, mis on vähemalt 6 tähemärki pikk",
"sendEmailLink": "Saada e-posti link",
"sendingEmailLink": "E-posti lingi saatmine...",
"sendLinkSuccessDescription": "Kontrolli oma e-posti, just saatsime sulle lingi. Järgi linki, et sisse logida.",
"sendLinkSuccess": "Saadame sulle lingi e-posti teel",
"sendLinkSuccessToast": "Link edukalt saadetud",
"getNewLink": "Hangi uus link",
"verifyCodeHeading": "Kinnita oma konto",
"verificationCode": "Kinnituskood",
"verificationCodeHint": "Sisesta SMS-iga saadetud kood",
"verificationCodeSubmitButtonLabel": "Sisesta kinnituskood",
"sendingMfaCode": "Kinnituskoodi saatmine...",
"verifyingMfaCode": "Koodi kontrollimine...",
"sendMfaCodeError": "Vabandust, meil ei õnnestunud saata kinnituskoodi",
"verifyMfaCodeSuccess": "Kood kinnitatud! Sisselogimine...",
"verifyMfaCodeError": "Ups! Paistab, et kood ei ole õige",
"reauthenticate": "Autentige uuesti",
"reauthenticateDescription": "Turvalisuse huvides peame teid uuesti autentima",
"errorAlertHeading": "Vabandust, me ei saanud sind autentida",
"emailConfirmationAlertHeading": "Saatsime sulle kinnituskirja e-posti teel.",
"emailConfirmationAlertBody": "Tere tulemast! Palun kontrolli oma e-posti ja klõpsa lingil, et konto kinnitada.",
"resendLink": "Saada link uuesti",
"resendLinkSuccessDescription": "Saime sulle saata uue lingi! Järgi linki, et sisse logida.",
"resendLinkSuccess": "Kontrolli oma e-posti!",
"authenticationErrorAlertHeading": "Autentimise viga",
"authenticationErrorAlertBody": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
"sendEmailCode": "Hangi kood e-posti teel",
"sendingEmailCode": "Koodi saatmine...",
"resetPasswordError": "Vabandust, me ei saanud parooli lähtestada. Palun proovi uuesti.",
"emailPlaceholder": "sinu@email.com",
"inviteAlertHeading": "Sind on kutsutud ettevõttega liituma",
"inviteAlertBody": "Palun logi sisse või registreeru, et kutse vastu võtta ja ettevõttega liituda.",
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
"termsOfService": "Kasutustingimused",
"privacyPolicy": "Privaatsuspoliitika",
"orContinueWith": "Või jätka koos",
"redirecting": "Oled sees! Palun oota...",
"signUpHeading": "Create an account",
"signUp": "Sign Up",
"signUpSubheading": "Fill the form below to create an account.",
"signInHeading": "Sign in to your account",
"signInSubheading": "Welcome back! Please enter your details",
"signIn": "Sign In",
"getStarted": "Get started",
"updatePassword": "Update Password",
"signOut": "Sign out",
"signingIn": "Signing in...",
"signingUp": "Signing up...",
"doNotHaveAccountYet": "Do not have an account yet?",
"alreadyHaveAnAccount": "Already have an account?",
"signUpToAcceptInvite": "Please sign in/up to accept the invite",
"clickToAcceptAs": "Click the button below to accept the invite with as <b>{{email}}</b>",
"acceptInvite": "Accept invite",
"acceptingInvite": "Accepting Invite...",
"acceptInviteSuccess": "Invite successfully accepted",
"acceptInviteError": "Error encountered while accepting invite",
"acceptInviteWithDifferentAccount": "Want to accept the invite with a different account?",
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead",
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead",
"signInWithProvider": "Sign in with {{provider}}",
"signInWithPhoneNumber": "Sign in with Phone Number",
"signInWithEmail": "Sign in with Email",
"signUpWithEmail": "Sign up with Email",
"passwordHint": "Ensure it's at least 8 characters",
"repeatPasswordHint": "Type your password again",
"repeatPassword": "Repeat password",
"passwordForgottenQuestion": "Password forgotten?",
"passwordResetLabel": "Reset Password",
"passwordResetSubheading": "Enter your email address below. You will receive a link to reset your password.",
"passwordResetSuccessMessage": "Check your Inbox! We emailed you a link for resetting your Password.",
"passwordRecoveredQuestion": "Password recovered?",
"passwordLengthError": "Please provide a password with at least 6 characters",
"sendEmailLink": "Send Email Link",
"sendingEmailLink": "Sending Email Link...",
"sendLinkSuccessDescription": "Check your email, we just sent you a link. Follow the link to sign in.",
"sendLinkSuccess": "We sent you a link by email",
"sendLinkSuccessToast": "Link successfully sent",
"getNewLink": "Get a new link",
"verifyCodeHeading": "Verify your account",
"verificationCode": "Verification Code",
"verificationCodeHint": "Enter the code we sent you by SMS",
"verificationCodeSubmitButtonLabel": "Submit Verification Code",
"sendingMfaCode": "Sending Verification Code...",
"verifyingMfaCode": "Verifying code...",
"sendMfaCodeError": "Sorry, we couldn't send you a verification code",
"verifyMfaCodeSuccess": "Code verified! Signing you in...",
"verifyMfaCodeError": "Ops! It looks like the code is not correct",
"reauthenticate": "Reauthenticate",
"reauthenticateDescription": "For security reasons, we need you to re-authenticate",
"errorAlertHeading": "Sorry, we could not authenticate you",
"emailConfirmationAlertHeading": "We sent you a confirmation email.",
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.",
"resendLink": "Resend link",
"resendLinkSuccessDescription": "We sent you a new link to your email! Follow the link to sign in.",
"resendLinkSuccess": "Check your email!",
"authenticationErrorAlertHeading": "Authentication Error",
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.",
"sendEmailCode": "Get code to your Email",
"sendingEmailCode": "Sending code...",
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
"emailPlaceholder": "your@email.com",
"inviteAlertHeading": "You have been invited to join a company",
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"orContinueWith": "Or continue with",
"redirecting": "You're in! Please wait...",
"errors": {
"Invalid login credentials": "Sisestatud andmed on valed",
"User already registered": "See konto on juba kasutusel. Palun proovi teisega.",
"Email not confirmed": "Palun kinnita oma e-posti aadress enne sisselogimist",
"default": "Tekkis viga. Palun veendu, et sul on töötav internetiühendus, ja proovi uuesti",
"generic": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
"link": "Vabandust, lingi saatmisel tekkis viga. Palun proovi uuesti.",
"codeVerifierMismatch": "Paistab, et proovid sisse logida teises brauseris kui see, millest lingi taotlesid. Palun proovi uuesti sama brauseriga.",
"minPasswordLength": "Parool peab olema vähemalt 8 tähemärki pikk",
"passwordsDoNotMatch": "Paroolid ei kattu",
"minPasswordNumbers": "Parool peab sisaldama vähemalt ühte numbrit",
"minPasswordSpecialChars": "Parool peab sisaldama vähemalt ühte erimärki",
"uppercasePassword": "Parool peab sisaldama vähemalt ühte suurtähte",
"insufficient_aal": "Palun logi sisse oma mitmeastmelise autentimisega, et seda toimingut teha",
"otp_expired": "E-posti link on aegunud. Palun proovi uuesti.",
"same_password": "Parool ei tohi olla sama, mis praegune parool"
"Invalid login credentials": "The credentials entered are invalid",
"User already registered": "This credential is already in use. Please try with another one.",
"Email not confirmed": "Please confirm your email address before signing in",
"default": "We have encountered an error. Please ensure you have a working internet connection and try again",
"generic": "Sorry, we weren't able to authenticate you. Please try again.",
"link": "Sorry, we encountered an error while sending your link. Please try again.",
"codeVerifierMismatch": "It looks like you're trying to sign in using a different browser than the one you used to request the sign in link. Please try again using the same browser.",
"minPasswordLength": "Password must be at least 8 characters long",
"passwordsDoNotMatch": "The passwords do not match",
"minPasswordNumbers": "Password must contain at least one number",
"minPasswordSpecialChars": "Password must contain at least one special character",
"uppercasePassword": "Password must contain at least one uppercase letter",
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action",
"otp_expired": "The email link has expired. Please try again.",
"same_password": "The password cannot be the same as the current password"
}
}

View File

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

View File

@@ -1,5 +1,5 @@
{
"recentlyCheckedDescription": "Siin on sinu olulisemad tervisenäitajad",
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
"respondToQuestion": "Vasta küsimusele",
"gender": "Sugu",
"male": "Mees",

View File

@@ -1,7 +0,0 @@
{
"invalidNumber": "Vigane arv",
"invalidEmail": "Vigane email",
"tooShort": "Liiga lühike sisend",
"tooLong": "Liiga pikk sisend",
"invalidPhone": "Vigane telefoninumber"
}

View File

@@ -1,41 +1,41 @@
{
"blog": "Blogi",
"blogSubtitle": "Uudised ja värskendused platvormi kohta",
"documentation": "Dokumentatsioon",
"documentationSubtitle": "Õpetused ja juhendid platvormiga alustamiseks",
"faq": "KKK",
"faqSubtitle": "Korduma kippuvad küsimused platvormi kohta",
"pricing": "Hinnakiri",
"pricingSubtitle": "Hinnaplaanid ja maksevõimalused",
"backToBlog": "Tagasi blogisse",
"noPosts": "Postitusi ei leitud",
"blogPaginationNext": "Järgmine leht",
"blogPaginationPrevious": "Eelmine leht",
"readMore": "Loe edasi",
"contactFaq": "Kui sul on küsimusi, võta meiega ühendust",
"contact": "Kontakt",
"about": "Meist",
"product": "Toode",
"legal": "Õiguslik teave",
"termsOfService": "Kasutustingimused",
"termsOfServiceDescription": "Meie tingimused ja nõuded",
"cookiePolicy": "Küpsiste kasutamise põhimõtted",
"cookiePolicyDescription": "Meie küpsiste kasutamise põhimõtted",
"privacyPolicy": "Privaatsuspoliitika",
"privacyPolicyDescription": "Meie privaatsuspoliitika ja kuidas me sinu andmeid kasutame",
"contactDescription": "Võta meiega ühendust küsimuste või tagasiside korral",
"contactHeading": "Saada meile sõnum",
"contactSubheading": "Vastame sulle esimesel võimalusel",
"contactName": "Sinu nimi",
"contactEmail": "Sinu e-post",
"contactMessage": "Sinu sõnum",
"sendMessage": "Saada sõnum",
"contactSuccess": "Sõnum on edukalt saadetud",
"contactError": "Sõnumi saatmisel tekkis viga",
"contactSuccessDescription": "Oleme sinu sõnumi kätte saanud ja võtame sinuga peagi ühendust",
"contactErrorDescription": "Sõnumi saatmisel tekkis viga. Palun proovi hiljem uuesti",
"footerDescription": "Siia saad lisada oma ettevõtte või toote kirjelduse",
"copyright": "© Copyright {{year}} {{product}}. Kõik õigused kaitstud.",
"blog": "Blog",
"blogSubtitle": "News and updates about the platform",
"documentation": "Documentation",
"documentationSubtitle": "Tutorials and guide to get started with the platform",
"faq": "FAQ",
"faqSubtitle": "Frequently asked questions about the platform",
"pricing": "Pricing",
"pricingSubtitle": "Pricing plans and payment options",
"backToBlog": "Back to blog",
"noPosts": "No posts found",
"blogPaginationNext": "Next Page",
"blogPaginationPrevious": "Previous Page",
"readMore": "Read more",
"contactFaq": "If you have any questions, please contact us",
"contact": "Contact",
"about": "About",
"product": "Product",
"legal": "Legal",
"termsOfService": "Terms of Service",
"termsOfServiceDescription": "Our terms and conditions",
"cookiePolicy": "Cookie Policy",
"cookiePolicyDescription": "Our cookie policy and how we use them",
"privacyPolicy": "Privacy Policy",
"privacyPolicyDescription": "Our privacy policy and how we use your data",
"contactDescription": "Contact us for any questions or feedback",
"contactHeading": "Send us a message",
"contactSubheading": "We will get back to you as soon as possible",
"contactName": "Your Name",
"contactEmail": "Your Email",
"contactMessage": "Your Message",
"sendMessage": "Send Message",
"contactSuccess": "Your message has been sent successfully",
"contactError": "An error occurred while sending your message",
"contactSuccessDescription": "We have received your message and will get back to you as soon as possible",
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
"footerDescription": "Here you can add a description about your company or product",
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
"heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest",
"notInterestedInAudit": "Ei soovi hetkel terviseauditit"
}

View File

@@ -9,20 +9,20 @@
"membersBillingButtonDescription": "Vali kuidas soovid eelarvet töötajate vahel jagada."
},
"settings": {
"pageTitle": "Seaded",
"pageDescription": "Halda oma ettevõtte andmeid",
"teamLogo": "Ettevõtte logo",
"teamLogoDescription": "Uuenda oma ettevõtte logo, et seda oleks lihtsam tuvastada",
"teamName": "Ettevõtte nimi",
"teamNameDescription": "Uuenda oma ettevõtte nime",
"dangerZone": "Ohtlik tsoon",
"dangerZoneDescription": "See osa sisaldab pöördumatuid toiminguid"
"pageTitle": "Settings",
"pageDescription": "Manage your Company details",
"teamLogo": "Company Logo",
"teamLogoDescription": "Update your company's logo to make it easier to identify",
"teamName": "Company Name",
"teamNameDescription": "Update your company's name",
"dangerZone": "Danger Zone",
"dangerZoneDescription": "This section contains actions that are irreversible"
},
"members": {
"pageTitle": "Töötajad"
"pageTitle": "Members"
},
"billing": {
"pageTitle": "Arveldamine"
"pageTitle": "Billing"
},
"benefitStatistics": {
"budget": {
@@ -50,148 +50,148 @@
"vitaminD": "Vitamiin D",
"smokers": "Suitsetajad"
},
"yourTeams": "Sinu ettevõtted ({{teamsCount}})",
"createTeam": "Loo ettevõte",
"creatingTeam": "Ettevõtet luuakse...",
"personalAccount": "Isiklik konto",
"searchAccount": "Otsi kontot...",
"membersTabLabel": "Töötajad",
"memberName": "Nimi",
"youLabel": "Sina",
"emailLabel": "E-post",
"roleLabel": "Roll",
"primaryOwnerLabel": "Peaadministraator",
"joinedAtLabel": "Liitus",
"invitedAtLabel": "Kutsutud",
"inviteMembersPageSubheading": "Kutsu töötajaid oma ettevõttesse",
"createTeamModalHeading": "Loo ettevõte",
"createTeamModalDescription": "Loo uus ettevõte, et hallata oma projekte ja töötajaid.",
"teamNameLabel": "Ettevõtte nimi",
"teamNameDescription": "Ettevõtte nimi peaks olema unikaalne ja kirjeldav",
"createTeamSubmitLabel": "Loo ettevõte",
"createTeamSuccess": "Ettevõte loodud edukalt",
"createTeamError": "Ettevõtet ei loodud. Palun proovi uuesti.",
"createTeamLoading": "Ettevõtet luuakse...",
"settingsPageLabel": "Üldine",
"createTeamDropdownLabel": "Uus ettevõte",
"changeRole": "Muuda rolli",
"removeMember": "Eemalda kontolt",
"inviteMembersSuccess": "Töötajad kutsutud edukalt!",
"inviteMembersError": "Vabandust, tekkis viga! Palun proovi uuesti",
"inviteMembersLoading": "Töötajaid kutsutakse...",
"removeInviteButtonLabel": "Eemalda kutse",
"addAnotherMemberButtonLabel": "Lisa veel üks",
"inviteMembersButtonLabel": "Saada kutsed",
"removeMemberModalHeading": "Sa eemaldad selle kasutaja",
"removeMemberModalDescription": "Eemalda see töötaja ettevõttest. Tal ei ole enam ligipääsu ettevõttele.",
"removeMemberSuccessMessage": "Töötaja eemaldatud edukalt",
"removeMemberErrorMessage": "Vabandust, tekkis viga. Palun proovi uuesti",
"removeMemberErrorHeading": "Vabandust, ei saanud valitud töötajat eemaldada.",
"removeMemberLoadingMessage": "Töötajat eemaldatakse...",
"removeMemberSubmitLabel": "Eemalda töötaja ettevõttest",
"chooseDifferentRoleError": "Roll on sama mis praegune",
"updateRole": "Uuenda rolli",
"updateRoleLoadingMessage": "Rolli uuendatakse...",
"updateRoleSuccessMessage": "Roll edukalt uuendatud",
"updatingRoleErrorMessage": "Vabandust, tekkis viga. Palun proovi uuesti.",
"updateMemberRoleModalHeading": "Uuenda töötaja rolli",
"updateMemberRoleModalDescription": "Muuda valitud töötaja rolli. Roll määrab töötaja õigused.",
"roleMustBeDifferent": "Roll peab erinema praegusest",
"memberRoleInputLabel": "Töötaja roll",
"updateRoleDescription": "Vali töötajale roll.",
"updateRoleSubmitLabel": "Uuenda rolli",
"transferOwnership": "Anna omandiõigus üle",
"transferOwnershipDescription": "Anna ettevõtte konto omandiõigus teisele töötajale.",
"transferOwnershipInputLabel": "Palun kirjuta TRANSFER, et kinnitada omandiõiguse üleandmist.",
"transferOwnershipInputDescription": "Kui annad omandiõiguse üle, ei ole sa enam ettevõtte konto peaadministraator.",
"deleteInvitation": "Kustuta kutse",
"deleteInvitationDialogDescription": "Oled kustutamas kutset. Kasutaja ei saa enam ettevõtte kontoga liituda.",
"deleteInviteSuccessMessage": "Kutse edukalt kustutatud",
"deleteInviteErrorMessage": "Kutse ei kustutatud. Palun proovi uuesti.",
"deleteInviteLoadingMessage": "Kutset kustutatakse. Palun oota...",
"confirmDeletingMemberInvite": "Oled kustutamas kutset aadressile <b>{{ email }}</b>",
"transferOwnershipDisclaimer": "Sa annad valitud ettevõtte konto omandiõiguse üle kasutajale <b>{{ member }}</b>.",
"transferringOwnership": "Omandiõigust antakse üle...",
"transferOwnershipSuccess": "Omandiõigus edukalt üle antud",
"transferOwnershipError": "Vabandust, ei saanud valitud töötajale omandiõigust üle anda. Palun proovi uuesti.",
"deleteInviteSubmitLabel": "Kustuta kutse",
"youBadgeLabel": "Sina",
"updateTeamLoadingMessage": "Ettevõtet uuendatakse...",
"updateTeamSuccessMessage": "Ettevõte edukalt uuendatud",
"updateTeamErrorMessage": "Ettevõtet ei saanud uuendada. Palun proovi uuesti.",
"updateLogoErrorMessage": "Logo ei saanud uuendada. Palun proovi uuesti.",
"teamNameInputLabel": "Ettevõtte nimi",
"teamLogoInputHeading": "Laadi üles oma ettevõtte logo",
"teamLogoInputSubheading": "Palun vali pilt, mida kasutada ettevõtte logona.",
"updateTeamSubmitLabel": "Uuenda ettevõtet",
"inviteMembersHeading": "Kutsu töötajaid oma ettevõttesse",
"inviteMembersDescription": "Kutsu töötajaid oma ettevõttesse sisestades nende e-posti ja rolli.",
"emailPlaceholder": "tootaja@email.com",
"membersPageHeading": "Töötajad",
"inviteMembersButton": "Kutsu töötajaid",
"invitingMembers": "Töötajaid kutsutakse...",
"inviteMembersSuccessMessage": "Töötajad kutsutud edukalt",
"inviteMembersErrorMessage": "Vabandust, töötajaid ei saanud kutsuda. Palun proovi uuesti.",
"pendingInvitesHeading": "Ootel kutsed",
"pendingInvitesDescription": " Siin saad hallata oma ettevõtte ootel kutseid.",
"noPendingInvites": "Ootel kutseid ei leitud",
"loadingMembers": "Töötajaid laaditakse...",
"loadMembersError": "Vabandust, ei saanud laadida ettevõtte töötajaid.",
"loadInvitedMembersError": "Vabandust, ei saanud laadida kutsutud töötajaid.",
"loadingInvitedMembers": "Kutsutud töötajaid laaditakse...",
"invitedBadge": "Kutsutud",
"duplicateInviteEmailError": "Oled juba sisestanud selle e-posti aadressi",
"invitingOwnAccountError": "Hei, see on sinu e-post!",
"dangerZone": "Ohtlik tsoon",
"dangerZoneSubheading": "Kustuta või lahku oma ettevõttest",
"deleteTeam": "Kustuta ettevõte",
"deleteTeamDescription": "Seda toimingut ei saa tagasi võtta. Kõik ettevõttega seotud andmed kustutatakse.",
"deletingTeam": "Ettevõtet kustutatakse",
"deleteTeamModalHeading": "Ettevõtte kustutamine",
"deletingTeamDescription": "Oled kustutamas ettevõtet {{ teamName }}. Seda toimingut ei saa tagasi võtta.",
"deleteTeamInputField": "Sisesta ettevõtte nimi kinnituseks",
"leaveTeam": "Lahku ettevõttest",
"leavingTeamModalHeading": "Ettevõttest lahkumine",
"leavingTeamModalDescription": "Oled lahkumas ettevõttest. Sul ei ole enam sellele ligipääsu.",
"leaveTeamDescription": "Vajuta allolevat nuppu, et ettevõttest lahkuda. Pea meeles, et sul ei ole enam ligipääsu ja pead uuesti kutsutuks saama.",
"deleteTeamDisclaimer": "Oled kustutamas ettevõtet {{ teamName }}. Seda toimingut ei saa tagasi võtta.",
"leaveTeamDisclaimer": "Oled lahkumas ettevõttest {{ teamName }}. Sul ei ole enam sellele ligipääsu.",
"deleteTeamErrorHeading": "Vabandust, ettevõtet ei saanud kustutada.",
"leaveTeamErrorHeading": "Vabandust, ettevõttest ei saanud lahkuda.",
"searchMembersPlaceholder": "Otsi töötajaid",
"createTeamErrorHeading": "Vabandust, ettevõtet ei saanud luua.",
"createTeamErrorMessage": "Ettevõtet luues tekkis viga. Palun proovi uuesti.",
"transferTeamErrorHeading": "Vabandust, ei saanud ettevõtte omandiõigust üle anda.",
"transferTeamErrorMessage": "Ettevõtte omandiõiguse üleandmisel tekkis viga. Palun proovi uuesti.",
"updateRoleErrorHeading": "Vabandust, ei saanud valitud töötaja rolli uuendada.",
"updateRoleErrorMessage": "Töötaja rolli uuendamisel tekkis viga. Palun proovi uuesti.",
"searchInvitations": "Otsi kutseid",
"updateInvitation": "Uuenda kutset",
"removeInvitation": "Eemalda kutse",
"acceptInvitation": "Võta kutse vastu",
"renewInvitation": "Uuenda kutset",
"resendInvitation": "Saada kutse uuesti",
"expiresAtLabel": "Aegub",
"expired": "Aegunud",
"active": "Aktiivne",
"inviteStatus": "Staatus",
"inviteNotFoundOrExpired": "Kutse ei leitud või on aegunud",
"inviteNotFoundOrExpiredDescription": "Kutse, mida otsid, on kas aegunud või ei eksisteeri. Palun võta ühendust ettevõtte administraatoriga, et kutset uuendada.",
"backToHome": "Tagasi avalehele",
"renewInvitationDialogDescription": "Oled uuendamas kutset aadressile {{ email }}. Kasutaja saab ettevõttega liituda.",
"renewInvitationErrorTitle": "Vabandust, ei saanud kutset uuendada.",
"renewInvitationErrorDescription": "Kutset uuendades tekkis viga. Palun proovi uuesti.",
"signInWithDifferentAccount": "Logi sisse teise kontoga",
"signInWithDifferentAccountDescription": "Kui soovid kutse vastu võtta teise kontoga, logi välja ja tagasi sisse soovitud kontoga.",
"acceptInvitationHeading": "Võta kutse vastu, et liituda ettevõttega {{accountName}}",
"acceptInvitationDescription": "Sind on kutsutud liituma ettevõttega {{accountName}}. Kui soovid kutse vastu võtta, vajuta allolevat nuppu.",
"continueAs": "Jätka kui {{email}}",
"joinTeamAccount": "Liitu ettevõttega",
"joiningTeam": "Ettevõttega liitumine...",
"leaveTeamInputLabel": "Palun kirjuta LEAVE kinnituseks, et ettevõttest lahkuda.",
"leaveTeamInputDescription": "Kui lahkud ettevõttest, ei ole sul enam sellele ligipääsu.",
"reservedNameError": "See nimi on reserveeritud. Palun vali mõni teine.",
"specialCharactersError": "Nimi ei tohi sisaldada erimärke. Palun vali mõni teine.",
"yourTeams": "Your Companies ({{teamsCount}})",
"createTeam": "Create a Company",
"creatingTeam": "Creating Company...",
"personalAccount": "Personal Account",
"searchAccount": "Search Account...",
"membersTabLabel": "Members",
"memberName": "Name",
"youLabel": "You",
"emailLabel": "Email",
"roleLabel": "Role",
"primaryOwnerLabel": "Primary Admin",
"joinedAtLabel": "Joined at",
"invitedAtLabel": "Invited at",
"inviteMembersPageSubheading": "Invite members to your Company",
"createTeamModalHeading": "Create Company",
"createTeamModalDescription": "Create a new Company to manage your projects and members.",
"teamNameLabel": "Company Name",
"teamNameDescription": "Your company name should be unique and descriptive",
"createTeamSubmitLabel": "Create Company",
"createTeamSuccess": "Company created successfully",
"createTeamError": "Company not created. Please try again.",
"createTeamLoading": "Creating company...",
"settingsPageLabel": "General",
"createTeamDropdownLabel": "New company",
"changeRole": "Change Role",
"removeMember": "Remove from Account",
"inviteMembersSuccess": "Members invited successfully!",
"inviteMembersError": "Sorry, we encountered an error! Please try again",
"inviteMembersLoading": "Inviting members...",
"removeInviteButtonLabel": "Remove invite",
"addAnotherMemberButtonLabel": "Add another one",
"inviteMembersButtonLabel": "Send Invites",
"removeMemberModalHeading": "You are removing this user",
"removeMemberModalDescription": "Remove this member from the company. They will no longer have access to the company.",
"removeMemberSuccessMessage": "Member removed successfully",
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
"removeMemberLoadingMessage": "Removing member...",
"removeMemberSubmitLabel": "Remove User from Company",
"chooseDifferentRoleError": "Role is the same as the current one",
"updateRole": "Update Role",
"updateRoleLoadingMessage": "Updating role...",
"updateRoleSuccessMessage": "Role updated successfully",
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
"updateMemberRoleModalHeading": "Update Member's Role",
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
"roleMustBeDifferent": "Role must be different from the current one",
"memberRoleInputLabel": "Member role",
"updateRoleDescription": "Pick a role for this member.",
"updateRoleSubmitLabel": "Update Role",
"transferOwnership": "Transfer Ownership",
"transferOwnershipDescription": "Transfer ownership of the company account to another member.",
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary admin of the company account.",
"deleteInvitation": "Delete Invitation",
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
"deleteInviteSuccessMessage": "Invite deleted successfully",
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
"transferOwnershipDisclaimer": "You are transferring ownership of the selected company account to <b>{{ member }}</b>.",
"transferringOwnership": "Transferring ownership...",
"transferOwnershipSuccess": "Ownership successfully transferred",
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
"deleteInviteSubmitLabel": "Delete Invite",
"youBadgeLabel": "You",
"updateTeamLoadingMessage": "Updating Company...",
"updateTeamSuccessMessage": "Company successfully updated",
"updateTeamErrorMessage": "Could not update Company. Please try again.",
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
"teamNameInputLabel": "Company Name",
"teamLogoInputHeading": "Upload your company's Logo",
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
"updateTeamSubmitLabel": "Update Company",
"inviteMembersHeading": "Invite Members to your Company",
"inviteMembersDescription": "Invite member to your company by entering their email and role.",
"emailPlaceholder": "member@email.com",
"membersPageHeading": "Members",
"inviteMembersButton": "Invite Members",
"invitingMembers": "Inviting members...",
"inviteMembersSuccessMessage": "Members invited successfully",
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
"pendingInvitesHeading": "Pending Invites",
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
"noPendingInvites": "No pending invites found",
"loadingMembers": "Loading members...",
"loadMembersError": "Sorry, we couldn't fetch your company's members.",
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited members.",
"loadingInvitedMembers": "Loading invited members...",
"invitedBadge": "Invited",
"duplicateInviteEmailError": "You have already entered this email address",
"invitingOwnAccountError": "Hey, that's your email!",
"dangerZone": "Danger Zone",
"dangerZoneSubheading": "Delete or leave your company",
"deleteTeam": "Delete Company",
"deleteTeamDescription": "This action cannot be undone. All data associated with this company will be deleted.",
"deletingTeam": "Deleting company",
"deleteTeamModalHeading": "Deleting Company",
"deletingTeamDescription": "You are about to delete the company {{ teamName }}. This action cannot be undone.",
"deleteTeamInputField": "Type the name of the company to confirm",
"leaveTeam": "Leave Company",
"leavingTeamModalHeading": "Leaving Company",
"leavingTeamModalDescription": "You are about to leave this company. You will no longer have access to it.",
"leaveTeamDescription": "Click the button below to leave the company. Remember, you will no longer have access to it and will need to be re-invited to join.",
"deleteTeamDisclaimer": "You are deleting the company {{ teamName }}. This action cannot be undone.",
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
"searchMembersPlaceholder": "Search members",
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
"searchInvitations": "Search Invitations",
"updateInvitation": "Update Invitation",
"removeInvitation": "Remove Invitation",
"acceptInvitation": "Accept Invitation",
"renewInvitation": "Renew Invitation",
"resendInvitation": "Resend Invitation",
"expiresAtLabel": "Expires at",
"expired": "Expired",
"active": "Active",
"inviteStatus": "Status",
"inviteNotFoundOrExpired": "Invite not found or expired",
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company admin to renew the invite.",
"backToHome": "Back to Home",
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
"signInWithDifferentAccount": "Sign in with a different account",
"signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.",
"acceptInvitationHeading": "Accept Invitation to join {{accountName}}",
"acceptInvitationDescription": "You have been invited to join the company {{accountName}}. If you wish to accept the invitation, please click the button below.",
"continueAs": "Continue as {{email}}",
"joinTeamAccount": "Join Company",
"joiningTeam": "Joining company...",
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
"reservedNameError": "This name is reserved. Please choose a different one.",
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.",
"personalCode": "Isikukood",
"teamOwnerPersonalCodeLabel": "Omaniku isikukood"
}
}

View File

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

View File

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

View File

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

View File

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